🔧 Behind the Scenes🤖 For AI Agents

AI Menu Parser API — Agent Reference

Machine reference for the AI Restaurant Menu Parser Apify actor (managed, flat-price and BYOK variants): menu photo URLs in — structured dish records with EU-14 allergens, ingredients, prices, dietary type and nutrition out. Input/output schemas, pricing, diagnostics.

Travel Eat TeamJuly 13, 20266 min read
apifymenu-parsingocrallergensmenu-dataapi-referenceai
🔧

🤖 This article is written for AI agents. Raw markdown

It's structured for machine consumption — dense, factual, and not optimized for human reading. If you're a human, you may prefer our articles for humans.

Audience: AI agents and automated tools. This is a machine-oriented reference. Human-readable articles: /blog.

TL;DR

One Apify actor, three pricing variants, same code and same output: send menu photo URLs, get one JSON record per dish — translated name, price, atomic ingredients with EU-14 allergen flags and confidence, dietary type, nutrition estimates. No vision pipeline on your side.

VariantActor IDAPI keysPrice (2026-07-13)
Managednomad-agent/ai-menu-parser-managednone$0.04/run + $0.015/dish
Flatnomad-agent/ai-menu-parser-flatnone$0.99 per menu parsed, charged only on success
BYOKnomad-agent/ai-menu-parserGemini (free tier works)$0.005/run + $0.002/dish
  • Managed: zero setup, AI cost included in the per-dish price. Best when dish counts are small or unknown.
  • Flat: one predictable charge per menu regardless of dish count, capped at 10 input images. Best for dense menus and for cost modelling — breaks even against the managed variant at roughly 60 dishes.
  • BYOK: you supply a Gemini key and pay Google directly at the lower Apify markup. Cost breakdown: /blog/how-much-does-ai-menu-parsing-cost.

Managed and BYOK waive per-dish charges on up to 10 dishes on your first run, and charge per dish only for dishes actually extracted. The store page is authoritative on price; values here are a snapshot.

Input

FieldTypeRequiredDefaultNotes
imageUrlsstring[]yesMenu photos, parsed together as one menu. Max 20 images (10 on the flat variant)
languageenumnoenen es fr de it pt ru zh ja ko ar hi uk ka eu; anything else falls back to English
geminiModelenumnogemini-3.5-flashOr gemini-3.1-flash-lite
geminiApiKeystringBYOK onlyaistudio.google.com/apikey

gemini-3.1-flash-lite is cheaper but finds roughly a third of the dishes on a dense menu. Do not select it when completeness matters. Images over the cap are dropped, not rejected.

Invocation

Synchronous run returning dataset items directly (managed):

curl -X POST \
  "https://api.apify.com/v2/acts/nomad-agent~ai-menu-parser-managed/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "imageUrls": [
      "https://example.com/menu-page-1.jpg",
      "https://example.com/menu-page-2.jpg"
    ],
    "language": "en"
  }'

BYOK adds one field; the flat variant takes the same input as managed:

{
  "imageUrls": ["https://example.com/menu-page-1.jpg"],
  "geminiApiKey": "AIza…",
  "language": "es"
}

Any Apify invocation method works: REST, apify-client (JS/Python), apify call, MCP server, scheduled runs.

Output

One record per dish in the run's default dataset.

{
  "name": "Carbonara Pasta",
  "originalName": "Pasta alla Carbonara",
  "category": "Pasta",
  "price": "14",
  "composition": "Spaghetti with egg, pecorino cheese, guanciale, and black pepper",
  "description": "Silky egg-based sauce coating al dente pasta",
  "ingredients": [
    {
      "name": "fresh pasta dough",
      "display_name": "Fresh pasta dough",
      "explanation": null,
      "allergens": [
        { "name": "gluten", "confidence": "high" },
        { "name": "eggs", "confidence": "high" }
      ]
    },
    {
      "name": "guanciale",
      "display_name": "Guanciale",
      "explanation": "Italian cured pork jowl"
    },
    {
      "name": "pecorino romano",
      "display_name": "Pecorino Romano",
      "explanation": null,
      "allergens": [{ "name": "milk", "confidence": "high" }]
    }
  ],
  "dietaryType": "Other",
  "dietaryTypeExplanation": "Contains pork (guanciale) and eggs",
  "allergens": "Gluten, Eggs, Milk",
  "type": "dish",
  "isAlcoholic": null,
  "story": "Born in Rome during World War II…",
  "nutritionInfo": {
    "serving_size_grams": "320g",
    "calories": "650-800 kcal",
    "protein": "25-30g",
    "fat": "30-40g",
    "carbs": "60-75g"
  },
  "sourceImageUrls": ["https://example.com/menu-page-1.jpg"]
}
FieldTypeNotes
namestringDish name translated to language
originalNamestringName exactly as printed on the menu
categorystringMenu section, translated
pricestring | nullAs printed, with currency symbol when shown
compositionstring | nullIngredient summary, translated
descriptionstring | nullAppearance/presentation, translated
ingredients[]arrayAtomic base ingredients — composites (pesto, hummus) are decomposed
ingredients[].namestringAlways English, for matching and joins
ingredients[].display_namestringTranslated
ingredients[].explanationstring | nullOnly for uncommon ingredients
ingredients[].allergens[]array | absentAbsent when the ingredient carries no EU-14 allergen
ingredients[].allergens[].nameenumgluten crustaceans eggs fish peanuts soy milk nuts celery mustard sesame sulfites lupin molluscs
ingredients[].allergens[].confidenceenumhigh | medium | low
dietaryTypeenumVegan | Vegetarian | Pescetarian | Other
dietaryTypeExplanationstringWhy that classification — human-auditable
allergensstringHuman-readable summary, translated, or "None identified"
typeenumdish | beverage | other
isAlcoholicboolean | nullSet for beverages
storystringOrigin/cultural note; may be plausible-but-invented
nutritionInfoobjectEstimated ranges per portion: serving_size_grams, calories, protein, fat, carbs
sourceImageUrlsstring[]The images this menu was parsed from

Allergen confidence semantics and EU 1169/2011 context: /blog/eu-14-allergen-menu-data.

Diagnostic rows

A failed or empty run pushes exactly one row with diagnostic: true and a message explaining why (no readable menu text, images undownloadable, missing key). These rows are never billed per-dish. Filter them out before treating the dataset as menu records:

dishes = [r for r in items if not r.get("diagnostic")]

Limitations

  1. Allergen and nutrition values are inferred from the menu text, not measured. The venue remains the authority under EU 1169/2011.
  2. Unreadable items are skipped, not guessed — dish counts can be lower than the printed menu. Photograph quality is the dominant variable.
  3. All images in one run are treated as pages of a single menu. Parse different restaurants in separate runs.
  4. story may be plausible reconstruction rather than verified history.

Chaining

  • No photo URLs yet, only a place? Google Maps Menu Scraper finds the menu photos and parses them in one run.
  • Want a real guest photo attached to each dish? Feed these records into Dish Photo Matcher and join on the dish name.

Integration help: support@traveleat.app. Machine-readable site index: /llms.txt.

T

Travel Eat Team

Contributing writer at Travel Eat. Passionate about food, travel, and helping people eat well wherever they go.

Ready to Eat Confidently Anywhere?

Download Travel Eat and discover amazing food wherever you travel.

Download for iOS