---
title: "Google Maps Menu Scraper API — Agent Reference"
date: "2026-07-13"
author: "Travel Eat Team"
excerpt: "Machine reference for the Google Maps Menu Scraper Apify actor (managed and BYOK variants): place URL, place ID, or place name in — structured dish records with EU-14 allergens, prices, ingredients, dietary type and nutrition out. Input/output schemas, pricing, diagnostics."
category: "Behind the Scenes"
tags: ["apify", "google-maps", "menu-data", "scraper", "allergens", "api-reference", "ai"]
featured: false
audience: "agents"
---

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

## TL;DR

One Apify actor, two variants, same code and same output: give it a Google Maps place (URL, `ChIJ…` place ID, or plain name) and it returns the restaurant's menu as one JSON record per dish — translated name, price, atomic ingredients with EU-14 allergen flags and confidence, dietary type, nutrition estimates.

| Variant | Actor ID | API keys | Price (2026-07-13) |
|---|---|---|---|
| Managed | [`nomad-agent/google-maps-menu-scraper-managed`](https://apify.com/nomad-agent/google-maps-menu-scraper-managed) | none | $0.02/run + $0.015/dish (drops to $0.012 on higher Apify plans) |
| BYOK | [`nomad-agent/google-maps-menu-scraper`](https://apify.com/nomad-agent/google-maps-menu-scraper) | Gemini (free tier works), Outscraper optional | $0.01/run + $0.004/dish (drops to $0.0025 on higher Apify plans) |

Choose **managed** for zero setup — AI and photo-fetch costs are included in the per-dish price. Choose **BYOK** when you already hold a Gemini key and want the lower Apify markup; you then pay Google directly. Cost breakdown: [/blog/how-much-does-ai-menu-parsing-cost](/blog/how-much-does-ai-menu-parsing-cost). The store page is authoritative on price; values here are a snapshot.

First run waives per-dish charges on up to 10 dishes. Per-dish fees apply only to dishes actually extracted; empty runs emit a free diagnostic row instead.

## How it resolves a menu

1. Input place is resolved to its Google Maps listing (name → Maps search, if needed).
2. The place's **Menu** photo gallery is fetched (`menuSource: "maps"`).
3. If Maps has no menu photos, the actor falls back to the menu published on the place's own website, PDFs included (`menuSource: "website"`).
4. Photos are deduplicated, then AI-parsed together as one menu at temperature 0 — the same menu bills the same way twice.
5. Nothing found → one non-billed diagnostic row explaining why.

Signed-out Google serves a "limited view" that hides the Maps photo gallery from plain browsers. The managed variant handles this on our keys. On BYOK, set `outscraperApiKey` ([free tier](https://app.outscraper.com)) for reliable gallery access; without it the actor falls back to browser scraping plus the website menu. Background: [/blog/google-maps-locked-photo-gallery-2026](/blog/google-maps-locked-photo-gallery-2026).

## Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `placeUrls` | string[] | yes | — | Maps URLs, bare place IDs (`ChIJ…`), or plain place names |
| `language` | enum | no | `en` | `en es fr de it pt ru zh ja ko ar hi uk ka eu` |
| `maxMenuPhotos` | integer | no | `8` | Clamped to 1–20 |
| `geminiModel` | enum | no | `gemini-3.5-flash` | Or `gemini-3.1-flash-lite` |
| `geminiApiKey` | string | BYOK only | — | [aistudio.google.com/apikey](https://aistudio.google.com/apikey) |
| `outscraperApiKey` | string | BYOK, recommended | — | Reliable Maps photo access |
| `proxyConfiguration` | object | BYOK only | — | Standard Apify proxy input |

`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.

## Invocation

Synchronous run returning dataset items directly (managed):

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/nomad-agent~google-maps-menu-scraper-managed/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "placeUrls": ["Katz'\''s Delicatessen New York"],
    "language": "en",
    "maxMenuPhotos": 8
  }'
```

BYOK adds the key:

```json
{
  "placeUrls": ["ChIJKZuMg9dZwokRA7zRe1Yh3Ok"],
  "geminiApiKey": "AIza…",
  "outscraperApiKey": "…",
  "language": "en"
}
```

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.

```json
{
  "place": "Katz's Delicatessen New York",
  "name": "Pastrami on Rye",
  "originalName": "Pastrami on Rye",
  "category": "Sandwiches",
  "price": "$26.95",
  "composition": "Hand-carved pastrami piled on rye bread with mustard",
  "description": "Towering sandwich served with a pickle spear",
  "ingredients": [
    {
      "name": "rye bread",
      "display_name": "Rye bread",
      "explanation": null,
      "allergens": [{ "name": "gluten", "confidence": "high" }]
    },
    {
      "name": "cured beef brisket",
      "display_name": "Cured beef brisket",
      "explanation": "Brined, spiced and smoked beef",
      "allergens": [{ "name": "sulfites", "confidence": "low" }]
    },
    {
      "name": "mustard",
      "display_name": "Mustard",
      "explanation": null,
      "allergens": [{ "name": "mustard", "confidence": "high" }]
    }
  ],
  "dietaryType": "Other",
  "dietaryTypeExplanation": "Contains beef",
  "allergens": "Gluten, Mustard, Sulfites",
  "type": "dish",
  "isAlcoholic": null,
  "story": "Katz's has cured its pastrami on Houston Street since 1888…",
  "nutritionInfo": {
    "serving_size_grams": "400g",
    "calories": "800-1000 kcal",
    "protein": "50-60g",
    "fat": "40-50g",
    "carbs": "50-60g"
  },
  "menuPhotoUrls": ["https://lh3.googleusercontent.com/p/AF1…"],
  "menuPhotosFound": 12,
  "menuSource": "maps"
}
```

| Field | Type | Notes |
|---|---|---|
| `place` | string | The place this dish came from — echoes your input, use it to join multi-place runs |
| `name` | string | Dish name translated to `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 — composites (pesto, hummus) are decomposed |
| `ingredients[].name` | string | Always English, for matching and joins |
| `ingredients[].display_name` | string | Translated |
| `ingredients[].explanation` | string \| null | Only for uncommon ingredients |
| `ingredients[].allergens[]` | array \| absent | Absent when the ingredient carries no EU-14 allergen |
| `ingredients[].allergens[].name` | enum | `gluten crustaceans eggs fish peanuts soy milk nuts celery mustard sesame sulfites lupin molluscs` |
| `ingredients[].allergens[].confidence` | enum | `high` \| `medium` \| `low` |
| `dietaryType` | enum | `Vegan` \| `Vegetarian` \| `Pescetarian` \| `Other` |
| `dietaryTypeExplanation` | string | Why that classification — human-auditable |
| `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 |
| `nutritionInfo` | object | Estimated ranges per portion |
| `menuPhotoUrls` | string[] | Photos actually parsed |
| `menuPhotosFound` | integer | Photos found before the `maxMenuPhotos` cut |
| `menuSource` | enum | `maps` \| `website` |

Allergen confidence semantics and EU 1169/2011 context: [/blog/eu-14-allergen-menu-data](/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 menu photos, photos undownloadable, model returned no dishes, missing key). These rows are never billed per-dish. Filter them out before treating the dataset as menu records:

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

## Limitations

1. Allergen and nutrition values are inferred from the menu, not measured. The venue remains the authority under EU 1169/2011.
2. Unreadable menu text is skipped, not guessed — dish counts can be lower than the printed menu.
3. Menu photos on Maps may be outdated; prices reflect the photographed menu, not today's.
4. `story` may be plausible reconstruction rather than verified history.

## Chaining

- Already hold menu photo URLs? Skip this actor and use [AI Menu Parser](/blog/ai-menu-parser-api).
- Want a real guest photo attached to each dish? Feed these records plus the same place into [Dish Photo Matcher](/blog/dish-photo-matcher-api) and join on the dish name.

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