# Dish Photo Matcher (No AI Markup): Food Photos to Menu Items (`traveleat/dish-photo-matcher`) Actor

$0.1/1K photos | Match restaurant food and drink photos to your menu items with Gemini vision. Send photo URLs or a Google Maps place URL (place mode needs your Outscraper key for gallery photos) plus your menu; unmatched photos return null. Your own Gemini key. Not affiliated with Google.

- **URL**: https://apify.com/traveleat/dish-photo-matcher.md
- **Developed by:** [Travel Eat](https://apify.com/traveleat) (community)
- **Categories:** AI, Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.05 / 1,000 photo matcheds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Dish Photo Matcher (No AI Markup): Food Photos to Menu Items

$0.1/1K photos | Match restaurant food and drink photos to your menu items with Gemini vision. Send photo URLs or a Google Maps place URL (place mode needs your Outscraper key for gallery photos) plus your menu; unmatched photos return null. Your own Gemini key. Not affiliated with Google.

### Restaurant Dish Photo Matcher

Our matcher takes your menu and a set of restaurant food or drink photos, and uses Gemini vision to say which menu item, if any, each photo shows. You get one record per photo with the exact menu item name, or `null` when nothing is a clear match. It runs on **your own Google AI Studio (Gemini) key**. It is ideal for enriching menus with real dish photos, labelling unlabeled Google Maps photos and shortlisting guest uploads for review.

- Don't want to manage keys? [Dish Photo Matcher (No API Key): Food Photos to Menu Items](https://apify.com/traveleat/dish-photo-matcher-managed) includes the AI and photo fetching, at $0.20 per 1,000 photos.
- Need the menu first? [AI Restaurant Menu Parser (No AI Markup) + Allergens](https://apify.com/traveleat/ai-menu-parser) turns menu photos into dish records this Actor accepts.
- Starting from a Google Maps place? Our [Google Maps Menu Scraper](https://apify.com/traveleat/google-maps-menu-scraper) extracts the structured menu.
- Browse all our scrapers: https://apify.com/traveleat

#### Key Benefits

- Your key, your cost: matching runs on your Gemini key, so the AI bill stays under your control (Google bills it separately).
- Low pricing: $0.10 per 1,000 photos, plus a $0.00005 start fee per run.
- Free to try: Start on the prefilled form returns a clearly marked DEMO row at no charge.
- Honest nulls: photos that are not clearly on your menu come back as `null`, and names not on your menu are filtered out.
- Your menu is the label set: results join straight back onto your own menu items.

### How It Works

1. Provide your menu in `menuItems`: objects with at least a `name` (`originalName`, `category` and `description` improve matching), or plain strings.
2. Provide the photos: upload JPEG, PNG or WebP files, pass direct `photoUrls`, or set `placeUrl` to a Google Maps place and the Actor matches its "Food & drink" guest photos. When `photoUrls` is set it wins over `placeUrl`.
3. Add your Gemini key, then run. Photos are matched in batches of 9.

More settings:

- `maxPhotos` - cap on photos matched, 1 to 100. Default is 9, which is one AI batch.
- `geminiModel` - model used for matching. Default is `gemini-3.1-flash-lite`.
- `outscraperApiKey` - optional, your own Outscraper key; recommended for place mode.
- `proxyConfiguration` - proxy for opening Google Maps in place mode. Residential is recommended.

The prefilled form (Katz's Delicatessen and a two-item menu, no keys) is a deterministic preview: it returns one DEMO row, scrapes nothing, calls no model and bills nothing. Add your Gemini key and your own menu for a live run.

Limits: Google hides the Maps photo gallery from automated browsers, so place mode is reliable only with your Outscraper key; the browser fallback reaches the cover photo only. AI matching can be wrong, so verify matches before publishing.

**AI matching can be wrong.** Verify matches before publishing or using them in ordering workflows, and never treat a visual match as evidence of ingredients, allergens or dietary suitability.

#### Data You'll Receive

- Match: `photoUrl` (or `photoRef` for uploaded images), `dishName` (the exact menu item name, or `null`) and `matched` (true or false).
- Place: the Google Maps place URL the photo came from, when you used place mode.
- Record type: `MATCH` for live results; `DEMO` for the free preview and `DIAGNOSTIC` for an explained empty run.

#### Sample output data

Here is an example row from a live run of the no-key sibling, which uses the same matching code:

```json
{
  "recordType": "MATCH",
  "photoUrl": "https://upload.wikimedia.org/wikipedia/commons/a/a3/Eq_it-na_pizza-margherita_sep2005_sml.jpg",
  "photoRef": "https://upload.wikimedia.org/wikipedia/commons/a/a3/Eq_it-na_pizza-margherita_sep2005_sml.jpg",
  "dishName": "Margherita Pizza",
  "matched": true,
  "place": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York"
}
```

### Dish Photo Matcher API

Results are stored in a dataset that you can export as CSV, JSON or Excel, or read on demand through the Apify API.

#### Support and Feedback

We improve these Actors from your feedback. If something breaks or you need a field:

- Open an issue on the Actor's Issues tab
- If this Actor saved you time, a short review on the Store helps other users find it.

### Run evidence

#### Start with one input and output

An Apify account is required. Use your Apify API token when calling the API; Console runs do not need that token pasted into the input. The unchanged default preview requires no provider key and makes no live AI or source call. A customized live run requires your own Gemini key; Maps/Outscraper collection also requires your own Outscraper key. Those providers bill separately.

Free-tier event rates checked 2026-10-08: `actor-start` $0.00005; `result` $0.0001.

**Cost:** Total = Actor event charges + platform usage if separately billed + any BYO-provider charges. See “Current costs” for your event rates, plan tiers and a calculation. A sample does not promise future billable volume.

This matching input/output pair is a **marked DEMO preview**, observed in run `cdz3hlg9kOJD7djHj` (immutable build `gw67DOllLQGe4h78U`). It demonstrates the output shape and makes no live source or AI-provider call. It does not verify a live parse or synchronization. Keep the preview unchanged; customize it and supply the documented credentials for live work.

**Input JSON**

```json
{
  "menuItems": [
    {
      "name": "Pastrami on Rye",
      "category": "Sandwiches"
    },
    {
      "name": "Matzo Ball Soup",
      "category": "Soups"
    }
  ],
  "placeUrl": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York",
  "geminiModel": "gemini-3.1-flash-lite",
  "maxPhotos": 9,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

**Matching output excerpt**

Long strings and arrays are shortened for display; this is not a complete dataset fixture.

```json
{
  "recordType": "DEMO",
  "isDemo": true,
  "demoSource": "owned-default-prefill",
  "demoNote": "Deterministic preview only: no live source, photo scrape, or Gemini call occurred, and the row was not billed. Customize the input for live output; BYOK listings also require their provider credentials.",
  "photoUrl": null,
  "photoRef": "demo:owned-default-prefill",
  "dishName": "DEMO SAMPLE — Pastrami on Rye",
  "matched": true,
  "place": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York"
}
```

Use AI vision to predict which supplied menu item, if any, appears in each
restaurant food or drink photo. Unmatched or uncertain photos remain `null`.
This actor joins menus and otherwise unlabeled guest photos:

- **Input**: your menu (JSON array of items) + uploaded food/drink photos,
  direct `photoUrls`, or a Google Maps `placeUrl` whose "Food & drink" guest
  photos are scraped automatically.
- **Output**: one record per photo — `{photoUrl, dishName, matched}`, with
  `dishName` the exact menu item name or `null` when nothing matches.

Photos are matched in batches of 9 by visual appearance; hallucinated names
(not on your menu) are filtered to `null` automatically. Ported from the
production photo-matching pipeline of a dietary-restrictions travel app.

#### Bring your own Gemini key

Matching runs on **your** Google AI Studio key
([get one free](https://aistudio.google.com/apikey)) — you control the AI
cost. A Gemini key is required for customized live runs. In place mode, an
Outscraper key is recommended for reliable Google Maps photo discovery.

#### Input example

```json
{
  "placeUrl": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York",
  "menuItems": [
    {"name": "Pastrami on Rye", "category": "Sandwiches"},
    {"name": "Matzo Ball Soup", "category": "Soups"}
  ],
  "geminiApiKey": "AIza…"
}
```

#### Notes

- The untouched keyless Console run returns one deterministic match example
  labelled `recordType: "DEMO"` / `isDemo: true`. It scrapes no photos, calls
  no model and has no Actor event charge. Customize the input and add your
  Gemini key; an Outscraper key is optional for place-photo lookup.
- `menuItems` accepts plain strings too: `["Pastrami on Rye", "Matzo Ball Soup"]`.
- The Console accepts local JPEG, PNG, or WebP uploads in `photoUrls`; API
  callers may also provide URLs, base64 data URIs, or raw base64.
- `photoUrl` remains a real sanitized HTTP(S) URL when one exists; inline
  images use `photoUrl: null` plus a non-sensitive `photoRef` fingerprint.
- **Place mode**: set the `outscraperApiKey` input
  ([free tier available](https://app.outscraper.com/profile)) — Google's
  signed-out "limited view" hides the Maps photo gallery from automated
  browsers, and Outscraper's API returns the full tagged photo set instead.
  The browser fallback (with the Apify residential proxy) currently reaches
  only the place's cover photo.
- Full Google Maps URLs and `maps.app.goo.gl` short links are accepted; short
  links are resolved only to trusted Google Maps destinations.
- Chain it: **Google Maps Menu Scraper** (or **AI Menu Parser**) extracts
  the structured menu, then this actor proposes photo-to-dish matches where
  the model finds one. Coverage is not guaranteed.
- AI photo matching can be wrong. Verify matches before publishing or using
  them in ordering workflows, and never treat a visual match as evidence of
  ingredients, allergens, or dietary suitability.

#### Pricing

Apify event pricing is **$0.00005 per successful live run plus $0.0001 per photo**
matched or confirmed unmatched (from 2026-10-08). A 9-photo live run is $0.00095 in Actor event
charges. Your Gemini usage and any Outscraper usage are billed separately by
those providers. The untouched deterministic demo and diagnostic-only runs
do not incur these Actor event charges.

#### Support & links

- Questions, bug reports and FAQ:
  [github.com/Exdenta/traveleat-support](https://github.com/Exdenta/traveleat-support)
- This actor runs the same menu pipeline as [Travel Eat](https://traveleat.app) —
  scan a menu, get allergens, ingredients and dietary filters in 36 languages.

#### Product guides and related tools

- [Product overview and public links](https://jobatlas.dev/actors/directory/menus-and-photos#nomad-agent-dish-photo-matcher)
- [Public setup guide](https://github.com/Exdenta/jobatlas/blob/main/docs/public-actors-setup.md)
- [Travel Eat menu API guide](https://traveleat.app/blog/ai-menu-parser-api)
- [Menu API documentation and support](https://github.com/Exdenta/traveleat-support)
- [Google Maps Menu Scraper — Place to Structured Menu API](https://apify.com/traveleat/google-maps-menu-scraper)
- [AI Restaurant Menu Parser — Dishes, Prices & Allergens](https://apify.com/traveleat/ai-menu-parser)

#### Current costs

Current Actor event rates, checked 2026-10-08. USD; use the **Pricing** tab for your plan and later changes.

| Event | Free | Bronze | Silver | Gold | Platinum | Diamond |
|---|---:|---:|---:|---:|---:|---:|
| Actor start (`actor-start`) | $0.00005 | $0.00005 | $0.00005 | $0.00005 | $0.00005 | $0.00005 |
| Photo matched (`result`) | $0.0001 | $0.0001 | $0.0001 | $0.0001 | $0.0001 | $0.0001 |

For example, 1 × `actor-start`, 100 × `result` totals **$0.01005 in Actor event charges on Free**. This is an arithmetic example, before any waiver; actual charged events depend on the run and selected options. It is not a promise of output or a charge for simply setting an item cap.

Your total is the charged Actor events, plus platform usage **only if this Actor’s Pricing tab bills it separately**, plus any external provider you use with your own key. Do not add platform usage twice when it is included. Apify credits and plan discounts affect your invoice. Set a run cost limit in Console before starting.

#### Agent setup skill

Use the maintained [SKILL.md](https://github.com/Exdenta/jobatlas/blob/main/.agents/skills/public-apify-actors/SKILL.md) to configure the documented inputs, select `latest`, record the immutable build and distinguish source results from preview or diagnostic rows. Check the current Pricing tab before starting a run.

***

**Independent tool.** We built and run this Actor ourselves. Google does not operate, sponsor or endorse it, and its name is used only to say which service the data is processed with. We are not affiliated with Google.

# Actor input Schema

## `menuItems` (type: `array`):

The restaurant's menu as a JSON array. Each item needs at least <code>name</code>; <code>originalName</code>, <code>category</code> and <code>description</code> improve match quality. Plain strings also work. Tip: the <b>Google Maps Menu Scraper</b> / <b>AI Menu Parser</b> actors produce compatible records.

## `photoUrls` (type: `array`):

Upload food/drink photos or provide direct image URLs (JPEG/PNG/WebP, 10 MiB each). API/JSON callers may also pass base64 data URIs or raw base64 strings. Leave empty to scrape photos from the Google Maps place below instead.

## `placeUrl` (type: `string`):

A full Google Maps place URL or maps.app.goo.gl short link; bare place IDs and names are also accepted. Its "Food & drink" guest photos are scraped and matched. Ignored when photoUrls is set.

## `geminiApiKey` (type: `string`):

Your Google AI Studio API key — get a free one at <a href='https://aistudio.google.com/apikey'>aistudio.google.com/apikey</a>. Matching runs on YOUR key, so you control the AI cost.

## `outscraperApiKey` (type: `string`):

API key from <a href='https://app.outscraper.com/profile'>app.outscraper.com</a> (free tier available). Google's signed-out "limited view" hides the Maps photo gallery from browsers, so this is the reliable way to fetch a place's tagged photos. Without it, the actor falls back to browser scraping, which currently reaches only the cover photo.

## `geminiModel` (type: `string`):

Model used to match photos to menu items.

## `maxPhotos` (type: `integer`):

Cap on how many photos are matched. The default is one 9-photo AI batch; increase it up to 100 for broader coverage and a higher run cost.

## `proxyConfiguration` (type: `object`):

Proxy used to open Google Maps (only in place mode). Google rate-limits datacenter IPs, so the Apify residential proxy is recommended.

## Actor input object example

```json
{
  "menuItems": [
    {
      "name": "Pastrami on Rye",
      "category": "Sandwiches"
    },
    {
      "name": "Matzo Ball Soup",
      "category": "Soups"
    }
  ],
  "placeUrl": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York",
  "geminiModel": "gemini-3.1-flash-lite",
  "maxPhotos": 9,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

The run's dataset items (see the dataset schema for the field-by-field structure).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "menuItems": [
        {
            "name": "Pastrami on Rye",
            "category": "Sandwiches"
        },
        {
            "name": "Matzo Ball Soup",
            "category": "Soups"
        }
    ],
    "placeUrl": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York",
    "maxPhotos": 9,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("traveleat/dish-photo-matcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "menuItems": [
        {
            "name": "Pastrami on Rye",
            "category": "Sandwiches",
        },
        {
            "name": "Matzo Ball Soup",
            "category": "Soups",
        },
    ],
    "placeUrl": "https://www.google.com/maps/place/Katz's+Delicatessen,+205+E+Houston+St,+New+York",
    "maxPhotos": 9,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("traveleat/dish-photo-matcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "menuItems": [
    {
      "name": "Pastrami on Rye",
      "category": "Sandwiches"
    },
    {
      "name": "Matzo Ball Soup",
      "category": "Soups"
    }
  ],
  "placeUrl": "https://www.google.com/maps/place/Katz'\''s+Delicatessen,+205+E+Houston+St,+New+York",
  "maxPhotos": 9,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call traveleat/dish-photo-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,traveleat/dish-photo-matcher"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/J5SoUe3wrDMRTOzvd/builds/zVsejhBhGIjNDI6GX/openapi.json
