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_....
X-API-Key: msk_your_api_key_here
Base URL
All API endpoints are relative to:
https://menusapi.com/api
Quick Start
Extract a restaurant menu in a single request:
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
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
| Parameter | Type | Description |
|---|---|---|
| url | string required | Restaurant URL to scrape (max 2048 chars) |
| location | string optional | Restaurant location for disambiguation (e.g. "San Diego, CA") |
| max_age_days | integer optional | Cache freshness tier. One of: 1, 14, 60 (default), 180. See Freshness Tiers. |
| google_place_id | string optional | Google Place ID for more precise cache matching |
| restaurant_name | string optional | Restaurant name hint to aid extraction |
Success Response 200
{
"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
Submit up to 500 restaurants for asynchronous processing. Returns a batch ID that you poll for results.
Request Body
| Parameter | Type | Description |
|---|---|---|
| restaurants | array required | Array of restaurant objects (max 500). Each must have url, and optionally location, restaurant_name, google_place_id. |
| max_age_days | integer optional | Cache freshness for the whole batch. One of: 1, 14, 60 (default), 180. |
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
Poll for batch job progress and results. When status is "completed", all items have been processed.
{
"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
Retrieve a paginated list of previously extracted menus, ordered by most recent.
| Query Param | Type | Description |
|---|---|---|
| per_page | integer optional | Results per page. Default 20, max 100. |
Get Menu by ID
Retrieve a specific menu by its ID. Returns the full menu data including metadata, completeness info, and extracted content.
Discover Sources
See what menu sources have been discovered for a given restaurant URL. Useful for debugging or understanding which sources the pipeline found.
| Query Param | Type | Description |
|---|---|---|
| url | string required | The restaurant URL to look up sources for |
Health Check
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.
| Value | Tier | Max Age | Best For |
|---|---|---|---|
| 180 | Long Cache | 180 days | Bulk imports, seeding databases |
| 60 | Medium (default) | 60 days | Regular updates, periodic refreshes |
| 14 | Short Cache | 14 days | Frequently changing menus |
| 1 | Fresh | 24 hours | Real-time accuracy, daily updates |
max_age_days is omitted, it defaults to 60 (Medium tier).
Menu Data Schema
The menu object in the response follows this structure:
{
"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
| Grade | Meaning | Billed? |
|---|---|---|
| complete | Full menu with prices and descriptions | Yes |
| good_no_prices | Full menu content, but prices not found on the source | Yes |
| partial_core_missing | Incomplete extraction, significant data missing | No |
| minimal | Very little useful data extracted | No |
| not_a_menu | The content was not a restaurant menu | No |
Source Types
The source_type field indicates where the menu data was extracted from:
WEBSITE— Restaurant's own websitePDF— Downloadable PDF menuIMAGE— Image-based menu (OCR)IFRAME— Embedded ordering systemDOORDASH,UBEREATS,GRUBHUB— Delivery platformsYELP— Yelp menu listingTOAST,SQUARE,CHOWNOW— POS/ordering platforms
Error Handling
| HTTP Status | Meaning |
|---|---|
| 200 | Success — menu data returned |
| 202 | Accepted — batch job created, poll for results |
| 400 | Bad request — missing or invalid parameters |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — API key deactivated |
| 404 | Not found — resource does not exist |
| 422 | Extraction failed — could not extract menu from URL |
"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
completeorgood_no_prices. - Non-billable: Failed extractions and results graded
partial_core_missing,minimal, ornot_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.