API Documentation

MenusAPI extracts structured restaurant menu data from any URL. Send a restaurant website, and get back clean JSON with categories, items, prices, and descriptions.

Authentication

All API requests (except health check) require an API key passed in the X-API-Key header.

Your API key is generated automatically when you subscribe and sent to your email. Keys use the format msk_....

Request Header
X-API-Key: msk_your_api_key_here
Keep your key secret. Do not expose it in client-side code, public repos, or share it with unauthorized parties. If your key is compromised, contact us to rotate it.

Base URL

All API endpoints are relative to:

https://menusapi.com/api

Quick Start

Extract a restaurant menu in a single request:

cURL
curl -X POST https://menusapi.com/api/scrape \
  -H "X-API-Key: msk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.example-restaurant.com",
    "location": "New York, NY",
    "max_age_days": 60
  }'

Scrape a Menu

POST /api/scrape

Submit a restaurant URL for menu extraction. Returns structured menu data as JSON. Results are cached based on the freshness tier you specify.

Request Body

ParameterTypeDescription
urlstring requiredRestaurant URL to scrape (max 2048 chars)
locationstring optionalRestaurant location for disambiguation (e.g. "San Diego, CA")
max_age_daysinteger optionalCache freshness tier. One of: 1, 14, 60 (default), 180. See Freshness Tiers.
google_place_idstring optionalGoogle Place ID for more precise cache matching
restaurant_namestring optionalRestaurant name hint to aid extraction

Success Response 200

Response JSON
{
  "success": true,
  "data": {
    "menu_id": "01jk...",
    "restaurant_name": "Joe's Pizza",
    "source_type": "WEBSITE",
    "extraction_method": "HTML_PARSE",
    "item_count": 47,
    "extracted_at": "2026-02-01T12:00:00+00:00",
    "menu": {
      "restaurant_name": "Joe's Pizza",
      "categories": [
        {
          "name": "Appetizers",
          "items": [
            {
              "name": "Garlic Knots",
              "description": "Fresh baked with garlic butter",
              "price": "$6.99"
            }
          ]
        }
      ]
    }
  },
  "extraction_confidence": { ... },
  "processing_time_ms": 8420
}

Error Response 422

{
  "success": false,
  "error": {
    "reason": "no_menu_found",
    "message": "Could not locate a menu on this website"
  },
  "attempt_id": "01jk...",
  "processing_time_ms": 15200
}

Batch Scrape

POST /api/scrape/batch

Submit up to 500 restaurants for asynchronous processing. Returns a batch ID that you poll for results.

Request Body

ParameterTypeDescription
restaurantsarray requiredArray of restaurant objects (max 500). Each must have url, and optionally location, restaurant_name, google_place_id.
max_age_daysinteger optionalCache freshness for the whole batch. One of: 1, 14, 60 (default), 180.
cURL
curl -X POST https://menusapi.com/api/scrape/batch \
  -H "X-API-Key: msk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "restaurants": [
      { "url": "https://restaurant-a.com", "location": "San Diego, CA" },
      { "url": "https://restaurant-b.com", "location": "Los Angeles, CA" }
    ],
    "max_age_days": 14
  }'

Response 202 Accepted

{
  "batch_id": "01jk...",
  "total": 2,
  "status": "processing",
  "poll_url": "/api/scrape/batch/01jk..."
}

Batch Status

GET /api/scrape/batch/{id}

Poll for batch job progress and results. When status is "completed", all items have been processed.

Response JSON
{
  "batch_id": "01jk...",
  "status": "completed",
  "total": 2,
  "completed": 2,
  "failed": 0,
  "pending": 0,
  "results": [
    {
      "url": "https://restaurant-a.com",
      "status": "completed",
      "menu_id": "01jk...",
      "restaurant_name": "Restaurant A",
      "item_count": 35,
      "processing_time_ms": 9800
    }
  ]
}

List Menus

GET /api/menus

Retrieve a paginated list of previously extracted menus, ordered by most recent.

Query ParamTypeDescription
per_pageinteger optionalResults per page. Default 20, max 100.

Get Menu by ID

GET /api/menus/{id}

Retrieve a specific menu by its ID. Returns the full menu data including metadata, completeness info, and extracted content.

Discover Sources

GET /api/sources?url={url}

See what menu sources have been discovered for a given restaurant URL. Useful for debugging or understanding which sources the pipeline found.

Query ParamTypeDescription
urlstring requiredThe restaurant URL to look up sources for

Health Check

GET /api/health

Public endpoint (no authentication required). Returns service status.

{
  "status": "ok",
  "timestamp": "2026-02-05T12:00:00+00:00"
}

Freshness Tiers

The max_age_days parameter controls how fresh the data must be. If a cached result exists within the age threshold, it's returned instantly. Otherwise, a fresh extraction is triggered.

ValueTierMax AgeBest For
180Long Cache180 daysBulk imports, seeding databases
60Medium (default)60 daysRegular updates, periodic refreshes
14Short Cache14 daysFrequently changing menus
1Fresh24 hoursReal-time accuracy, daily updates
If max_age_days is omitted, it defaults to 60 (Medium tier).

The menu object in the response follows this structure:

Menu JSON Schema
{
  "restaurant_name": "string",
  "categories": [
    {
      "name": "string",
      "description": "string | null",
      "items": [
        {
          "name": "string",
          "description": "string | null",
          "price": "string | null",
          "variants": [
            { "name": "string", "price": "string" }
          ],
          "dietary_info": ["string"]
        }
      ]
    }
  ],
  "metadata": {
    "has_prices": true,
    "has_descriptions": true,
    "estimated_item_count": 47,
    "detected_language": "en"
  },
  "completeness": {
    "grade": "complete",
    "flags": []
  }
}

Completeness Grades

GradeMeaningBilled?
completeFull menu with prices and descriptionsYes
good_no_pricesFull menu content, but prices not found on the sourceYes
partial_core_missingIncomplete extraction, significant data missingNo
minimalVery little useful data extractedNo
not_a_menuThe content was not a restaurant menuNo

Source Types

The source_type field indicates where the menu data was extracted from:

  • WEBSITE — Restaurant's own website
  • PDF — Downloadable PDF menu
  • IMAGE — Image-based menu (OCR)
  • IFRAME — Embedded ordering system
  • DOORDASH, UBEREATS, GRUBHUB — Delivery platforms
  • YELP — Yelp menu listing
  • TOAST, SQUARE, CHOWNOW — POS/ordering platforms

Error Handling

HTTP StatusMeaning
200Success — menu data returned
202Accepted — batch job created, poll for results
400Bad request — missing or invalid parameters
401Unauthorized — missing or invalid API key
403Forbidden — API key deactivated
404Not found — resource does not exist
422Extraction failed — could not extract menu from URL
Cached failures: If a URL previously failed extraction, subsequent requests within the same freshness window will return the cached failure immediately (with "cached": true) to avoid expensive re-processing. Wait for the cache to expire, or use a shorter max_age_days value.

Billing Rules

  • Billable requests: Successful extractions with completeness grades of complete or good_no_prices.
  • Non-billable: Failed extractions and results graded partial_core_missing, minimal, or not_a_menu.
  • Cache hits are billable: You pay for the freshness tier, not the extraction time. A cache hit at the Fresh tier costs the same as a fresh extraction at the Fresh tier.
  • Billing is per-request: Usage is metered and reported to Stripe monthly alongside your plan's base fee.

See Pricing for full per-request rates by plan and freshness tier.