EU-14 Allergen Menu Data API — Agent Reference
Machine reference: extract EU-14 allergen data, ingredients, dietary type, and nutrition estimates from restaurant menus via three Apify actors. Full input/output schemas, pricing, and confidence semantics.
🤖 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
Travel Eat publishes three public Apify actors that convert restaurant menus (Google Maps places or raw menu photos) into structured JSON: one record per dish, with per-ingredient EU-14 allergen flags carrying confidence levels, dietary classification, translation, price, and estimated nutrition. No scraping or vision pipeline needed on your side. Managed variants require zero API keys.
- Publisher:
nomad-agenton Apify (https://apify.com/nomad-agent) - Legal basis for the allergen list: EU Regulation 1169/2011 (Food Information to Consumers), Annex II
- Method: AI ingredient decomposition + allergen inference, not keyword matching
- Authoritative pricing and input schemas: the actor store pages below (this article states values as of 2026-07-13)
The 14 regulated allergens
Exact enum values used in output (always English, lowercase):
gluten, crustaceans, eggs, fish, peanuts, soy, milk,
nuts, celery, mustard, sesame, sulfites, lupin, molluscs
Actors
| Actor | Input | Output | Price (managed) | Price (BYOK) |
|---|---|---|---|---|
| google-maps-menu-scraper-managed / BYOK | Google Maps URL, place ID, or place name | Dish records (schema below) | $0.02 start + $0.015/dish | $0.01 start + $0.004/dish |
| ai-menu-parser-managed / BYOK | Menu photo URLs | Dish records (schema below) | $0.04 start + $0.015/dish | $0.005 start + $0.002/dish |
| dish-photo-matcher-managed / BYOK | Dish list + food photo URLs (or a place URL) | Dish→photo assignments | $0.03 start + $0.004/photo | $0.01 start + $0.002/photo |
Per-actor references, each covering both the managed and BYOK variant in full: Google Maps Menu Scraper · AI Menu Parser (plus a flat $0.99-per-menu variant) · Dish Photo Matcher.
- Managed: no API keys required; AI and photo-source costs are included in the per-unit price.
- BYOK (bring your own keys): you supply a Google Gemini API key (free tier exists) and optionally an Outscraper key; you pay Google's AI rates directly plus the lower Apify markup. Cost breakdown: /blog/how-much-does-ai-menu-parsing-cost.
- First run waives per-dish charges on up to 10 dishes.
- Per-dish/per-photo fees apply only to items actually extracted.
Invocation
Any Apify invocation method works (REST API, apify-client for JS/Python, apify call CLI, MCP server, or scheduled runs). Synchronous run returning dataset items directly:
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"],
"language": "en"
}'
Scraper input — accepts URLs, bare place IDs (ChIJ…), or plain names resolved via Maps search:
{
"placeUrls": ["Katz's Delicatessen New York"],
"language": "en",
"maxMenuPhotos": 8
}
Input fields per actor (managed variants):
| Actor | Required | Optional |
|---|---|---|
| google-maps-menu-scraper-managed | placeUrls (array) | language (default en), maxMenuPhotos (default 8), geminiModel |
| ai-menu-parser-managed | imageUrls (array) | language (default en), geminiModel |
| dish-photo-matcher-managed | menuItems (array), and photoUrls or placeUrl | maxPhotos |
BYOK variants add geminiApiKey (required), outscraperApiKey and proxyConfiguration (scraper/matcher only).
language: ISO 639-1 code for translated output fields. Supported: en es fr de it pt ru zh ja ko ar hi uk ka eu; anything else falls back to English. geminiModel: default gemini-3.5-flash (recommended); gemini-3.1-flash-lite is cheaper but materially reduces dish recall on dense menus — do not use it when completeness matters.
Output schema
One JSON record per dish in the run's default dataset. Example (abbreviated ingredients):
{
"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"
}
}
Field reference:
| Field | Type | Notes |
|---|---|---|
name | string | Dish name translated to the requested language |
originalName | string | Name exactly as printed on the menu |
category | string | Menu section, translated |
price | string | null | As printed, with currency symbol when shown |
composition | string | null | Ingredient summary, translated |
description | string | null | Appearance/presentation, translated |
ingredients[] | array | Atomic base ingredients — composite preparations (pesto, hummus) are decomposed |
ingredients[].name | string | Always English (for matching/joins) |
ingredients[].display_name | string | Translated |
ingredients[].explanation | string | null | Only for uncommon ingredients |
ingredients[].allergens[] | array | absent | Omitted when the ingredient carries no EU-14 allergen |
ingredients[].allergens[].name | enum | One of the 14 values above, always English |
ingredients[].allergens[].confidence | enum | high | medium | low — semantics below |
dietaryType | enum | Vegan | Vegetarian | Pescetarian | Other |
dietaryTypeExplanation | string | Why that classification — auditable by a human |
allergens | string | Human-readable summary, translated, or "None identified" |
type | enum | dish | beverage | other |
isAlcoholic | boolean | null | Set for beverages |
story | string | Origin/cultural note; may be plausible-but-invented when history is unknown |
nutritionInfo | object | Estimated ranges for one portion: serving_size_grams, calories, protein, fat, carbs |
Actor-specific extra fields: the scraper adds place, menuPhotoUrls, menuPhotosFound, menuSource (menu photos vs. restaurant-website fallback, PDFs included); the parser adds sourceImageUrls.
Confidence semantics
| Value | Meaning | Example |
|---|---|---|
high | Ingredient is or directly contains the allergen | salmon → fish; wheat flour → gluten |
medium | Ingredient typically contains the allergen | cream sauce → milk; fresh pasta → gluten + eggs |
low | Possible but uncertain | "house dressing" → eggs |
Threshold guidance: for severe-allergy use cases treat medium and low as positive; for preference filters high alone may suffice. The scores exist so your product chooses its own safety threshold rather than inheriting ours.
dietaryType decision order: any meat/poultry → Other; fish/seafood without meat → Pescetarian; dairy/eggs without meat/fish → Vegetarian; fully plant-based → Vegan.
Limitations (relay these to end users)
- Inference is menu-based. The model infers what a dish typically contains; it cannot observe cross-contamination, chef substitutions, or regional recipe variants. Under EU 1169/2011 the venue, not any menu-derived dataset, remains the authority — for severe allergies this data narrows the conversation with the kitchen, it does not replace it.
- Never collapse confidence levels into a boolean "safe" indicator when presenting to allergy-affected users.
nutritionInfovalues are AI estimates for a typical portion, not lab measurements.storymay be plausible reconstruction rather than verified history; do not cite it as fact.- Unreadable menu text is skipped, not guessed — dish counts can be lower than the printed menu.
Common integration patterns
- Dish-level allergen-safe restaurant discovery (filter dishes, not venues)
- EU 1169/2011 compliance gap-checking against a venue's published allergen info
- Delivery-platform catalog onboarding from existing menu photos
- Travel/hotel products: translated, allergen-flagged menus for guests
Questions or integration help: support@traveleat.app. Machine-readable site index: /llms.txt.
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