{
  "openapi": "3.0.3",
  "info": {
    "title": "Tabecal Japan Chain Restaurant Nutrition API",
    "version": "1.1.0",
    "description": "Official published nutrition facts (kcal, protein, fat, carbohydrate, salt equivalent, etc.), prices, allergen tables, menu change history and store locations for about 78 Japanese restaurant / fast-food / convenience-store chains and ~19,000 menu items. Data is collected daily from each chain's official website — no estimated values. Null means the chain does not publish that field.\n\n日本の外食・中食チェーン約78社・約19,000品の公式公表栄養成分・価格・アレルゲン・改定履歴・店舗座標。毎営業日更新。推定値なし。\n\n**Attribution required**: 「データ: タベカル（tabecal.com）／各社公式公表値」. Store coordinates © OpenStreetMap contributors (ODbL).",
    "termsOfService": "https://tabecal.com/api/#terms",
    "contact": { "name": "Tabecal", "url": "https://tabecal.com/contact/" },
    "x-logo": { "url": "https://tabecal.com/icon-512.png" }
  },
  "servers": [ { "url": "https://tabecal.com/api/v1" } ],
  "security": [ {}, { "ApiKeyHeader": [] }, { "BearerKey": [] } ],
  "tags": [
    { "name": "chains", "description": "Covered chains" },
    { "name": "items", "description": "Menu items and nutrition" },
    { "name": "changes", "description": "Menu change history (new / discontinued / changed)" },
    { "name": "allergens", "description": "28 Japanese labelling allergens" },
    { "name": "stores", "description": "Store locations (OpenStreetMap)" },
    { "name": "meta", "description": "Status and documentation" }
  ],
  "paths": {
    "/": { "get": { "tags": ["meta"], "summary": "API description, enumerations and data status", "operationId": "getRoot",
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } } } },
    "/health": { "get": { "tags": ["meta"], "summary": "Health check (item count, data date). Not counted against quota", "operationId": "getHealth",
      "security": [ {} ],
      "responses": { "200": { "description": "Data is fresh", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } } },
                     "503": { "description": "Data older than 3 days or unavailable" } } } },
    "/chains": { "get": { "tags": ["chains"], "summary": "List chains with coverage stats", "operationId": "listChains",
      "parameters": [ { "$ref": "#/components/parameters/genre" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Envelope" }, { "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Chain" } } } } ] },
        "example": { "meta": { "api": "tabecal v1", "plan": "anonymous", "data_date": "2026-08-27" }, "data": [ { "slug": "yoshinoya", "name_ja": "吉野家", "genre": "gyudon", "genre_ja": "牛丼・丼", "items": 312, "kcal_avg": 521, "with_protein": 312, "with_salt": 312, "with_price": 0, "with_allergens": 250, "last_change": "2026-08-20" } ] } } } },
                     "429": { "$ref": "#/components/responses/TooMany" } } } },
    "/chains/{slug}": { "get": { "tags": ["chains"], "summary": "Chain overview (categories, recent events, coverage)", "operationId": "getChain",
      "parameters": [ { "$ref": "#/components/parameters/slug" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } },
    "/chains/{slug}/items": { "get": { "tags": ["chains", "items"], "summary": "All current items of one chain", "operationId": "listChainItems",
      "parameters": [ { "$ref": "#/components/parameters/slug" }, { "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/category" }, { "$ref": "#/components/parameters/sort" }, { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/offset" } ],
      "responses": { "200": { "$ref": "#/components/responses/ItemList" }, "404": { "$ref": "#/components/responses/NotFound" }, "429": { "$ref": "#/components/responses/TooMany" } } } },
    "/items": { "get": { "tags": ["items"], "summary": "Search items by name and nutrition limits", "operationId": "searchItems",
      "description": "All filters are ANDed. `q` accepts space-separated words (each must match name, category or chain). Sorting puts items with a null sort value last.",
      "parameters": [
        { "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/chain" }, { "$ref": "#/components/parameters/genre" }, { "$ref": "#/components/parameters/category" },
        { "name": "kcal_min", "in": "query", "schema": { "type": "number" } }, { "name": "kcal_max", "in": "query", "schema": { "type": "number" }, "example": 600 },
        { "name": "protein_min", "in": "query", "schema": { "type": "number" }, "example": 20 }, { "name": "protein_max", "in": "query", "schema": { "type": "number" } },
        { "name": "fat_max", "in": "query", "schema": { "type": "number" } }, { "name": "carb_max", "in": "query", "schema": { "type": "number" } },
        { "name": "sugar_max", "in": "query", "schema": { "type": "number" } }, { "name": "fiber_min", "in": "query", "schema": { "type": "number" } },
        { "name": "salt_max", "in": "query", "schema": { "type": "number" }, "description": "Salt equivalent 食塩相当量 (g)", "example": 3 },
        { "name": "price_min", "in": "query", "schema": { "type": "number" } }, { "name": "price_max", "in": "query", "schema": { "type": "number" } },
        { "name": "allergen_free", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated allergens to exclude (items published as contains / may_contain). e.g. 卵,乳,小麦", "example": "卵,乳" },
        { "name": "known", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "With allergen_free: only items whose chain publishes allergen data and lists it as none" },
        { "name": "has", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated fields that must be non-null, e.g. protein_g,salt_g" },
        { "$ref": "#/components/parameters/sort" }, { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/offset" } ],
      "responses": { "200": { "$ref": "#/components/responses/ItemList" }, "400": { "$ref": "#/components/responses/BadRequest" }, "429": { "$ref": "#/components/responses/TooMany" } } } },
    "/items/{item_key}": { "get": { "tags": ["items"], "summary": "Item detail with version history, events, allergens and attributes", "operationId": "getItem",
      "parameters": [ { "name": "item_key", "in": "path", "required": true, "schema": { "type": "string" }, "example": "2d6481c6a7201eab" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Envelope" }, { "properties": { "data": { "$ref": "#/components/schemas/ItemDetail" } } } ] } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } },
    "/changes": { "get": { "tags": ["changes"], "summary": "Menu changes (added / changed / removed), newest first", "operationId": "listChanges",
      "parameters": [
        { "name": "since", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Default: 30 days ago" }, { "name": "until", "in": "query", "schema": { "type": "string", "format": "date" } },
        { "$ref": "#/components/parameters/chain" }, { "name": "event", "in": "query", "schema": { "type": "string", "enum": ["added", "changed", "removed"] } },
        { "name": "field", "in": "query", "schema": { "type": "string" }, "description": "For changed: which field (kcal, price, salt_g ...)" }, { "$ref": "#/components/parameters/q" },
        { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/offset" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Envelope" }, { "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Change" } } } } ] } } } }, "429": { "$ref": "#/components/responses/TooMany" } } } },
    "/allergens": { "get": { "tags": ["allergens"], "summary": "Allergen rows (one row per item × allergen)", "operationId": "listAllergens",
      "parameters": [ { "$ref": "#/components/parameters/chain" }, { "$ref": "#/components/parameters/q" },
        { "name": "contains", "in": "query", "schema": { "type": "string" }, "description": "Only rows where this allergen is 'contains'", "example": "卵" },
        { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["contains", "may_contain", "none", "unknown"] } },
        { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/offset" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Envelope" }, { "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/AllergenRow" } } } } ] } } } } } } },
    "/allergens/lookup": { "get": { "tags": ["allergens"], "summary": "Allergen statement of one item, grouped by status", "operationId": "lookupAllergens",
      "parameters": [ { "name": "item_key", "in": "query", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/chain" }, { "name": "name", "in": "query", "schema": { "type": "string" }, "description": "Item name (partial) when item_key is unknown" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" },
        "example": { "meta": { "api": "tabecal v1" }, "data": { "item": { "chain": "吉野家", "name": "牛丼", "size": "並盛" }, "allergens": { "contains": ["小麦", "牛肉", "大豆"], "may_contain": [], "none": ["卵", "乳"], "unknown": [], "not_listed": [] } } } } } }, "404": { "$ref": "#/components/responses/NotFound" } } } },
    "/stats/daily": { "get": { "tags": ["chains"], "summary": "Daily per-chain statistics (counts, averages, adds/changes/removals)", "operationId": "dailyStats",
      "parameters": [ { "$ref": "#/components/parameters/chain" }, { "name": "since", "in": "query", "schema": { "type": "string", "format": "date" } }, { "$ref": "#/components/parameters/limit" }, { "$ref": "#/components/parameters/offset" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Envelope" } } } } } } },
    "/stores": { "get": { "tags": ["stores"], "summary": "Stores near a point or by prefecture / city", "operationId": "listStores",
      "parameters": [ { "name": "lat", "in": "query", "schema": { "type": "number" }, "example": 35.6812 }, { "name": "lon", "in": "query", "schema": { "type": "number" }, "example": 139.7671 },
        { "name": "radius_km", "in": "query", "schema": { "type": "number", "default": 3, "maximum": 50 } }, { "$ref": "#/components/parameters/chain" },
        { "name": "pref", "in": "query", "schema": { "type": "string" }, "example": "東京都" }, { "name": "city", "in": "query", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/limit" } ],
      "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Envelope" }, { "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Store" } } } } ] } } } } } } }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-Api-Key" },
      "BearerKey": { "type": "http", "scheme": "bearer" }
    },
    "parameters": {
      "slug": { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Chain slug (see /chains) or Japanese name", "example": "yoshinoya" },
      "chain": { "name": "chain", "in": "query", "schema": { "type": "string" }, "description": "Chain slug or Japanese name", "example": "sukiya" },
      "genre": { "name": "genre", "in": "query", "schema": { "type": "string", "enum": ["burger", "cafe", "conveni", "curry", "family", "gyudon", "health", "izakaya", "noodle", "pizza", "sushi", "sweets", "teishoku", "yakiniku"] } },
      "category": { "name": "category", "in": "query", "schema": { "type": "string" }, "description": "Official menu category (partial match)" },
      "q": { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Name search (Japanese; space-separated words are ANDed)", "example": "牛丼" },
      "sort": { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["name", "kcal", "kcal_desc", "protein", "protein_asc", "salt", "salt_desc", "price", "price_desc", "updated", "protein_per_kcal"], "default": "name" } },
      "limit": { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 50 }, "description": "Max 50 without key, 1000 with key" },
      "offset": { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
    },
    "responses": {
      "ItemList": { "description": "OK", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Envelope" }, { "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Item" } } } } ] },
        "example": { "meta": { "api": "tabecal v1", "plan": "anonymous", "data_date": "2026-08-27", "total": 3, "limit": 50, "offset": 0, "attribution": "データ: タベカル（https://tabecal.com/）" },
                     "data": [ { "item_key": "5a1c0e9d2b7f4a10", "chain": "吉野家", "chain_slug": "yoshinoya", "genre": "gyudon", "name": "牛丼", "size": "並盛", "category": "牛丼", "kcal": 635.0, "protein_g": 20.0, "fat_g": 23.4, "carb_g": 89.0, "fiber_g": null, "sugar_g": null, "salt_g": 2.7, "sodium_mg": null, "caffeine_mg": null, "weight_g": null, "price": null, "valid_from": "2026-08-16" } ] } } } },
      "NotFound": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "BadRequest": { "description": "Invalid parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "TooMany": { "description": "Daily quota exceeded (anonymous: 200/day/IP)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Envelope": { "type": "object", "properties": { "meta": { "$ref": "#/components/schemas/Meta" }, "data": {} } },
      "Meta": { "type": "object", "properties": { "api": { "type": "string" }, "version": { "type": "string" }, "plan": { "type": "string" }, "data_date": { "type": "string", "format": "date", "description": "Date of the latest daily update" }, "generated": { "type": "string" }, "source": { "type": "string" }, "attribution": { "type": "string" }, "docs": { "type": "string" }, "total": { "type": "integer" }, "limit": { "type": "integer" }, "offset": { "type": "integer" } } },
      "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "integer" }, "message": { "type": "string" } } } } },
      "Health": { "type": "object", "properties": { "ok": { "type": "boolean" }, "version": { "type": "string" }, "items": { "type": "integer" }, "chains": { "type": "integer" }, "data_date": { "type": "string" }, "history_from": { "type": "string" }, "allergen_chains": { "type": "integer" }, "stores": { "type": "integer" } } },
      "Chain": { "type": "object", "properties": { "slug": { "type": "string" }, "name_ja": { "type": "string" }, "genre": { "type": "string" }, "genre_ja": { "type": "string" }, "items": { "type": "integer" }, "kcal_avg": { "type": "number", "nullable": true }, "salt_avg": { "type": "number", "nullable": true }, "protein_avg": { "type": "number", "nullable": true }, "with_protein": { "type": "integer", "description": "Items with protein published" }, "with_salt": { "type": "integer" }, "with_price": { "type": "integer" }, "with_allergens": { "type": "integer" }, "last_change": { "type": "string", "format": "date" } } },
      "Item": { "type": "object", "properties": {
        "item_key": { "type": "string", "description": "Permanent id of item × size (16 hex)" }, "chain": { "type": "string" }, "chain_slug": { "type": "string" }, "genre": { "type": "string" },
        "name": { "type": "string" }, "size": { "type": "string" }, "category": { "type": "string" },
        "kcal": { "type": "number", "nullable": true }, "protein_g": { "type": "number", "nullable": true }, "fat_g": { "type": "number", "nullable": true }, "carb_g": { "type": "number", "nullable": true },
        "fiber_g": { "type": "number", "nullable": true }, "sugar_g": { "type": "number", "nullable": true, "description": "糖質" }, "salt_g": { "type": "number", "nullable": true, "description": "食塩相当量 (salt equivalent), not sodium" },
        "sodium_mg": { "type": "number", "nullable": true }, "caffeine_mg": { "type": "number", "nullable": true }, "weight_g": { "type": "number", "nullable": true }, "price": { "type": "number", "nullable": true, "description": "JPY, tax included, only where published" },
        "valid_from": { "type": "string", "format": "date", "description": "First day these values were confirmed" } } },
      "ItemDetail": { "type": "object", "properties": { "item": { "$ref": "#/components/schemas/Item" }, "history": { "type": "array", "items": { "type": "object" } }, "events": { "type": "array", "items": { "$ref": "#/components/schemas/Change" } }, "allergens": { "type": "array", "items": { "type": "object", "properties": { "allergen": { "type": "string" }, "status": { "type": "string" }, "source_file": { "type": "string" } } } }, "attributes": { "type": "array", "items": { "type": "object", "properties": { "attr": { "type": "string" }, "value": { "type": "string" } } } } } },
      "Change": { "type": "object", "properties": { "event_date": { "type": "string", "format": "date" }, "event": { "type": "string", "enum": ["added", "changed", "removed"] }, "chain": { "type": "string" }, "chain_slug": { "type": "string", "nullable": true }, "name": { "type": "string" }, "size": { "type": "string" }, "field": { "type": "string", "nullable": true }, "old_value": { "type": "string", "nullable": true }, "new_value": { "type": "string", "nullable": true }, "item_key": { "type": "string" } } },
      "AllergenRow": { "type": "object", "properties": { "chain": { "type": "string" }, "name": { "type": "string" }, "size": { "type": "string" }, "item_key": { "type": "string", "nullable": true }, "allergen": { "type": "string" }, "status": { "type": "string", "enum": ["contains", "may_contain", "none", "unknown"] }, "source_file": { "type": "string" } } },
      "Store": { "type": "object", "properties": { "osm_type": { "type": "string" }, "osm_id": { "type": "integer" }, "chain_slug": { "type": "string" }, "brand": { "type": "string" }, "name": { "type": "string" }, "branch": { "type": "string", "nullable": true }, "lat": { "type": "number" }, "lon": { "type": "number" }, "addr_province": { "type": "string", "nullable": true }, "addr_city": { "type": "string", "nullable": true }, "postcode": { "type": "string", "nullable": true }, "opening_hours": { "type": "string", "nullable": true }, "distance_km": { "type": "number" }, "osm_url": { "type": "string" } } }
    }
  },
  "x-mcp": { "url": "https://tabecal.com/api/mcp", "transport": "streamable-http", "tools": ["search_menu_items", "get_menu_item", "list_chains", "get_chain", "recent_menu_changes", "check_allergens", "find_stores_nearby"] }
}
