---
title: "Dish Photo Matcher API — Agent Reference"
date: "2026-07-13"
author: "Travel Eat Team"
excerpt: "Machine reference for the Restaurant Dish Photo Matcher Apify actor (managed and BYOK variants): a menu plus food photos in — one photo→dish assignment per photo out, matched by AI vision against your own label set. Input/output schemas, pricing, diagnostics."
category: "Behind the Scenes"
tags: ["apify", "google-maps", "photo-matching", "computer-vision", "menu-data", "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 menu (list of items) plus food photos — either direct URLs or a Google Maps place whose guest "Food & drink" photos are fetched for you — and it returns one record per photo, labelled with the exact menu item it shows, or `null` when it shows nothing on that menu.

| Variant | Actor ID | API keys | Price (2026-07-13) |
|---|---|---|---|
| Managed | [`nomad-agent/dish-photo-matcher-managed`](https://apify.com/nomad-agent/dish-photo-matcher-managed) | none | $0.03/run + $0.004/photo |
| BYOK | [`nomad-agent/dish-photo-matcher`](https://apify.com/nomad-agent/dish-photo-matcher) | Gemini (free tier works), Outscraper optional | $0.01/run + $0.002/photo |

The label space is *your* menu, not a generic food taxonomy — that is the difference from an image-classification API, and from a raw Maps photo scraper that returns unlabelled URLs. A default run (27 photos) costs about $0.14 managed. The store page is authoritative on price; values here are a snapshot.

## How it works

1. Photos come from `photoUrls` if set, otherwise from the `placeUrl`'s Google Maps "Food & drink" gallery. When both are set, `photoUrls` wins.
2. Photos are matched in batches of 9 by visual appearance (Gemini Flash-Lite vision).
3. Names the model returns that are not on your menu are filtered to `null` — hallucinated labels cannot reach the dataset.
4. Every analyzed photo is billed, matched or not: confirming a non-match costs the same inference as a match.

On BYOK, set `outscraperApiKey` ([free tier](https://app.outscraper.com)) when using `placeUrl` — signed-out Google hides the Maps photo gallery from plain browsers. The managed variant handles this on our keys. Background: [/blog/google-maps-locked-photo-gallery-2026](/blog/google-maps-locked-photo-gallery-2026).

## Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `menuItems` | array | yes | — | Objects (`{name, originalName?, category?, description?}`) or plain strings. Extra fields improve match quality |
| `photoUrls` | string[] | one of these | — | Direct food/drink photo URLs |
| `placeUrl` | string | one of these | — | Google Maps place URL, place ID, or name — its guest food photos are fetched |
| `maxPhotos` | integer | no | `27` | Clamped to 1–100 |
| `geminiApiKey` | string | BYOK only | — | [aistudio.google.com/apikey](https://aistudio.google.com/apikey) |
| `outscraperApiKey` | string | BYOK, recommended | — | Needed for reliable `placeUrl` gallery access |
| `geminiModel` | enum | BYOK only | `gemini-3.1-flash-lite` | Vision matching does not need a reasoning model |
| `proxyConfiguration` | object | BYOK only | — | Standard Apify proxy input |

## Invocation

Place mode — the actor fetches the photos itself (managed):

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/nomad-agent~dish-photo-matcher-managed/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "placeUrl": "Katz'\''s Delicatessen New York",
    "menuItems": [
      { "name": "Pastrami on Rye", "category": "Sandwiches" },
      { "name": "Matzo Ball Soup", "category": "Soups" }
    ],
    "maxPhotos": 27
  }'
```

Photo mode — you already hold the image URLs, and BYOK:

```json
{
  "photoUrls": ["https://example.com/photo-1.jpg", "https://example.com/photo-2.jpg"],
  "menuItems": ["Pastrami on Rye", "Tiramisu"],
  "geminiApiKey": "AIza…"
}
```

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

## Output

One record per photo analyzed.

```json
{
  "photoUrl": "https://lh3.googleusercontent.com/p/AF1…",
  "dishName": "Pastrami on Rye",
  "matched": true,
  "place": "Katz's Delicatessen New York"
}
```

| Field | Type | Notes |
|---|---|---|
| `photoUrl` | string | The photo that was analyzed |
| `dishName` | string \| null | Exact `name` of the matched menu item, or `null` when the photo matches nothing on the menu |
| `matched` | boolean | `false` is a real answer, not an error — galleries contain interiors, drinks, off-menu dishes |
| `place` | string \| null | Only present in place mode |
| `diagnostic` | boolean | Only on a non-billed diagnostic row |
| `message` | string | Only on a diagnostic row |

`dishName` is guaranteed to be a string that exists in your `menuItems`, so you can join on it without fuzzy matching:

```python
by_dish = {}
for r in items:
    if r.get("diagnostic") or not r["matched"]:
        continue
    by_dish.setdefault(r["dishName"], []).append(r["photoUrl"])
```

## Diagnostic rows

A failed or empty run pushes exactly one row with `diagnostic: true` and a `message` (no photos found for the place, photos undownloadable, missing key). These rows are never billed per-photo. Filter on `diagnostic` before consuming the dataset.

## Limitations

1. Matching is by appearance. Visually near-identical dishes on the same menu (two burgers, two red pastas) can be swapped.
2. Guest photos are not menu photography: bad light, half-eaten plates and mislabelled uploads all reduce accuracy.
3. Unmatched photos are billed. Cap cost with `maxPhotos` rather than by filtering afterwards.
4. Tuned for food and drink. It generalises to any photo-against-known-label-list task, but nothing else is tested.

## Chaining

Both menu actors emit records this actor accepts directly as `menuItems`:

1. Run [Google Maps Menu Scraper](/blog/google-maps-menu-scraper-api) on a place (or [AI Menu Parser](/blog/ai-menu-parser-api) on your photos) → structured dishes.
2. Pass those dishes plus the same place into this actor.
3. Join on `dishName` — every dish now carries a real guest photo.

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