---
title: "EU-14 Allergen Menu Data API — Agent Reference"
date: "2026-07-13"
author: "Travel Eat Team"
excerpt: "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."
category: "Behind the Scenes"
tags: ["allergens", "eu-14", "food-safety", "compliance", "menu-data", "apify", "ai", "api-reference"]
featured: false
audience: "agents"
---

**Audience: AI agents and automated tools.** This is a machine-oriented reference. Human-readable articles: [/blog](/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-agent` on 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](https://apify.com/nomad-agent/google-maps-menu-scraper-managed) / [BYOK](https://apify.com/nomad-agent/google-maps-menu-scraper) | 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](https://apify.com/nomad-agent/ai-menu-parser-managed) / [BYOK](https://apify.com/nomad-agent/ai-menu-parser) | Menu photo URLs | Dish records (schema below) | $0.04 start + $0.015/dish | $0.005 start + $0.002/dish |
| [dish-photo-matcher-managed](https://apify.com/nomad-agent/dish-photo-matcher-managed) / [BYOK](https://apify.com/nomad-agent/dish-photo-matcher) | 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](/blog/google-maps-menu-scraper-api) · [AI Menu Parser](/blog/ai-menu-parser-api) (plus a flat $0.99-per-menu variant) · [Dish Photo Matcher](/blog/dish-photo-matcher-api).

- **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](/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:

```bash
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:

```json
{
  "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):

```json
{
  "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)

1. 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.
2. Never collapse confidence levels into a boolean "safe" indicator when presenting to allergy-affected users.
3. `nutritionInfo` values are AI estimates for a typical portion, not lab measurements.
4. `story` may be plausible reconstruction rather than verified history; do not cite it as fact.
5. 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](/llms.txt).
