Menu & Nutrition Label OCR: Prices, Allergens, Nutrients avatar

Menu & Nutrition Label OCR: Prices, Allergens, Nutrients

Pricing

from $10.00 / 1,000 page reads

Go to Apify Store
Menu & Nutrition Label OCR: Prices, Allergens, Nutrients

Menu & Nutrition Label OCR: Prices, Allergens, Nutrients

Menu OCR and nutrition facts label reader: photos, scans and PDFs of restaurant menus, US and EU nutrition panels and food labels to structured data. One row per dish with numeric prices, currency, section and allergens; nutrients with Open Food Facts keys, re-checked by code. Pay per page.

Pricing

from $10.00 / 1,000 page reads

Rating

0.0

(0)

Developer

Alexander Daskalov

Alexander Daskalov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

9 days ago

Last modified

Security

Limited permissions

Share

Turn photos, scans and PDFs of restaurant menus, nutrition facts panels and food labels into clean structured data. Paste links to your files (JPG, PNG, HEIC iPhone photos, PDF…, or Google Drive and Dropbox file links) and get:

  • 🍝 Menus: one row per dish with the section, description, every price as a number with its ISO currency (sizes like 8,50 € / 11,00 € become two prices), dietary marks (vegetarian, vegan, gluten-free…) and allergens resolved through the menu's own legend.
  • 🥣 Nutrition panels (US FDA and EU): every nutrient per 100 g and per serving, % Daily Value, serving size in grams, and the values as Open Food Facts fields (fat_100g, salt_serving…), ready to import.
  • 🏷️ Product labels: brand, product name, net quantity, ingredients, the allergens printed in bold, the "may contain" allergens, best-before and use-by dates (ISO when unambiguous), lot number, manufacturer and the barcode decoded from the bars with its check digit verified.
  • ✅ Every number re-checked by code: label arithmetic (energy from fat, carbs and protein, kJ vs kcal, sugars within carbohydrates, per-serving vs per-100 g, FDA calorie rounding, % Daily Values), the GS1 barcode check digit and, for PDF menus, the PDF's own text. Failed checks are reported in checks.failed, never silently "fixed".
  • 📄 Multi-page PDFs: sections continue across page breaks, and a legend on the last page explains the marks on the first.

No API key, no account to connect, no setup. Mixed files are fine: each page is recognised as a menu, a nutrition panel, a product label or something else.

What can you do with it?

  • Food delivery and ordering platforms: onboard restaurants from a photo or PDF of their menu instead of retyping every dish and price.
  • Menu price monitoring: compare prices across restaurants, cities or over time, with numbers and currencies you can sort and chart.
  • Nutrition and diet apps: read nutrition facts panels into your food database with Open Food Facts keys.
  • Allergen and dietary filters: find the vegan, gluten-free or nut-free dishes on a menu, as the menu prints them.
  • Retail and e-commerce catalogues: fill product pages with ingredients, allergens, net quantity and the barcode from a label photo.
  • Food label compliance checks: spot labels whose energy doesn't add up, whose sugars exceed carbohydrates or whose barcode check digit is wrong.
  • AI assistants and RAG: give your assistant structured menus and labels instead of raw OCR text.

What files does it accept?

InputExampleSupported
Direct image file linkhttps://example.com/menus/lunch.jpg✅ JPG, PNG, WebP, HEIC/HEIF (iPhone), TIFF incl. multi-page, GIF, BMP
Direct PDF linkhttps://example.com/menus/dinner.pdf✅ printed (born-digital) or scanned PDFs, page by page
Cloud storage file URLa public file URL from S3 or another bucket✅ if it opens without logging in
Google Drive filehttps://drive.google.com/file/d/…/view✅ shared as "Anyone with the link"
Dropbox filehttps://www.dropbox.com/scl/fi/…?dl=0✅
Restaurant websites, Google Maps places, folders❌ link the photo or PDF itself

Options: What the files show (detect per page, or menus / nutrition panels / product labels only), Currency for unmarked prices (an ISO code such as USD for menus that print $9.99 or no sign at all; a printed €, £, ¥… always wins) and Max pages per file (default 10, up to 50).

How much does it cost?

Pay per page, plus a small fee per menu row. No monthly fee.

EventPrice (FREE/BRONZE)Lower on paid plans
Page read (menu, nutrition panel or product label)$0.012 / pagedown to $0.010
Menu item (one dish or drink with its prices)$0.0005 / item—
Actor start$0.00005 / run—

Examples: a nutrition label or product label costs $0.012; a menu photo with 30 dishes costs $0.027 ($0.012 + 30 × $0.0005); a 4-page PDF menu with 120 dishes costs $0.108.

You pay only for what you get: pages with nothing to read (say, a photo of the restaurant's front door), pages the AI couldn't read and files that can't be opened are not charged.

Cap your spend: set a maximum cost per run. Pages are sized to it before they are read; if it runs out in the middle of a menu, you get the rows it pays for and the last one is marked "truncated": true.

Free plan: the AI runs on paid infrastructure, so free-plan runs include a 3-page trial per run; upgrade to read more pages.

How to read a menu or a nutrition label

  1. Click Try for free. The example input reads a sample menu and a sample nutrition label.
  2. Paste your own links into Photo, scan or PDF links, one per line.
  3. Optional: set What the files show, a Currency for unmarked prices, and Max pages per file for long PDFs.
  4. Run, then open the Menu items or Labels and pages view, or download JSON, CSV or Excel.

Input example

{
"fileUrls": [
"https://example.com/menus/dinner.pdf",
"https://example.com/labels/granola-back.jpg"
],
"documentType": "auto",
"currency": "USD",
"maxPagesPerFile": 10
}

Output

A menu gives one record per dish. From a 2-page Italian PDF menu:

{
"url": "https://example.com/menus/trattoria.pdf",
"source": "direct",
"page": 1,
"pageCount": 2,
"documentType": "menu",
"recordType": "menu-item",
"itemIndex": 5,
"restaurantName": "Trattoria Esempio",
"section": "Pizze",
"name": "Margherita",
"description": "Pomodoro, fior di latte, basilico",
"prices": [
{"amount": 8.5, "currency": "EUR", "text": "8,50 €"},
{"amount": 11, "currency": "EUR", "text": "11,00 €"}
],
"price": 8.5,
"currency": "EUR",
"priceNote": null,
"dietary": ["vegetarian"],
"allergens": ["gluten", "milk"],
"allergenText": null,
"marks": ["V", "1", "7"],
"truncated": false,
"checks": {"passed": ["text-layer-match"], "failed": []},
"processedAt": "2026-10-16T00:22:40+00:00"
}

The allergens come from the numbers 1, 7 printed next to the dish and the menu's own legend ("1 glutine, 7 latte"); V is resolved the same way. A price printed as text only ("prezzo di mercato", "market price") gives price: null and the text in priceNote.

A nutrition panel or product label gives one record per page. From a German biscuit label (nutrients shortened):

{
"url": "https://example.com/labels/haferkekse.png",
"source": "direct",
"page": 1,
"pageCount": 1,
"documentType": "product",
"recordType": "product",
"format": "eu",
"servingSizeText": null,
"servingSizeG": 12.5,
"servingsPerContainer": null,
"nutrients": [
{"name": "Energie", "key": "energy-kj", "unit": "kJ", "per100g": 1989, "perServing": 249, "dailyValuePercent": null},
{"name": "Energie", "key": "energy-kcal", "unit": "kcal", "per100g": 475, "perServing": 59, "dailyValuePercent": null},
{"name": "Fett", "key": "fat", "unit": "g", "per100g": 21, "perServing": 2.6, "dailyValuePercent": null}
],
"off": {
"energy-kj_100g": 1989, "energy-kcal_100g": 475, "fat_100g": 21, "saturated-fat_100g": 9.5,
"carbohydrates_100g": 62, "sugars_100g": 24, "fiber_100g": 4.5, "proteins_100g": 7.2, "salt_100g": 0.55,
"sodium_100g": 0.22, "fat_serving": 2.6, "salt_serving": 0.07
},
"brand": "Beispiel",
"productName": "Haferkekse",
"netQuantityText": "200 g",
"netQuantity": {"value": 200, "unit": "g"},
"ingredientsText": "Haferflocken (38 %), Weizenmehl, Zucker, Butter (Milch), Haselnüsse (8 %), Backtriebmittel: Natriumcarbonate, Salz. Kann Spuren von Soja enthalten.",
"emphasisedAllergens": ["Haferflocken", "Weizenmehl", "Milch", "Haselnüsse"],
"allergens": ["gluten", "milk", "nuts"],
"allergensMayContain": ["soybeans"],
"barcode": {"digits": "2000123456782", "type": "EAN-13", "valid": true, "from": "decoded"},
"bestBeforeText": "31.12.2027",
"bestBefore": "2027-12-31",
"useByText": null,
"useBy": null,
"lot": "L 24318",
"manufacturer": "Beispiel Backwaren GmbH, Musterstraße 1, 12345 Musterstadt",
"checks": {
"passed": ["kj-kcal-ratio", "energy-from-macros", "saturates-within-fat", "sugars-within-carbohydrates",
"macros-within-100g", "serving-matches-100g", "barcode-check-digit"],
"failed": []
},
"processedAt": "2026-10-16T02:03:31+00:00"
}

Every record has the same fields (those that don't apply are null), so menus and labels can share one spreadsheet. recordType is menu-item, nutrition, product, other (a page with nothing to read, not charged) or error (a file that couldn't be used, with the reason in error, not charged):

{"url": "https://example.com/menus/old.pdf", "recordType": "error", "error": "file not available (HTTP 404): is the link public?"}

Field notes:

  • off uses grams for every nutrient (sodium too) and kJ / kcal for energy, like Open Food Facts; salt and sodium fill each other (salt = sodium × 2.5).
  • nutrients[].unit and the amounts are as printed (mg, mcg…).
  • emphasisedAllergens lists the ingredient words whose allergen part is printed in bold or capitals, as printed.
  • allergens uses the EU-14 names: gluten, crustaceans, eggs, fish, peanuts, soybeans, milk, nuts, celery, mustard, sesame, sulphites, lupin, molluscs.
  • barcode.from is decoded when the bars were read from the image, or read when only the printed digits were legible (then valid tells you if the check digit adds up).
  • Dates are converted only when they can be read one way (31.12.2027, 2027-12-31); 01/02/27 stays as text.

Which checks are run?

CheckWhat it catches
energy-from-macrosenergy that doesn't match fat, carbohydrates and protein (EU factors 9/4/4/2 within 15%, US 9/4/4 within 20%): a misread digit in any of them
kj-kcal-ratiokJ and kcal that don't match each other
saturates-within-fat, sugars-within-carbohydrates, added-within-sugarsa part larger than its whole
macros-within-100gmore than 100 g of nutrients in 100 g
serving-matches-100ga per-serving column that doesn't match the per-100 g column and the serving size
fda-calorie-rounding, daily-valuesUS panels: calories not rounded as the FDA requires, % Daily Values that don't match the amounts
barcode-check-digita barcode whose GS1 check digit is wrong
text-layer-matchPDF menus with a text layer: the dish name and its prices must appear in the PDF's own text

How accurate is it?

Measured on Apify with Google's Gemini 3.1 Flash-Lite model, on sample files we made with exact known answers:

  • An English menu photo (10 dishes, 11 prices, dietary marks, allergen notes): every dish, price, section, mark and allergen right.
  • A 2-page Italian PDF menu (19 dishes, 29 prices in 8,50 € style, size variants, a section continuing on page 2, a numbered allergen legend): all 19 dishes and 29 prices right, every row confirmed by the PDF's text layer.
  • A US Nutrition Facts panel, as a clean image and as a tilted, blurred iPhone HEIC photo: all 15 nutrients right on both, in repeated runs, and every % Daily Value right except that a 0% was sometimes left empty.
  • Two EU labels (English and German, per 100 g and per portion): all 18 values right on each; the barcode decoded and valid.
  • A label with deliberate errors (wrong kJ, sugars above carbohydrates, a wrong barcode digit): every error flagged in checks.failed.

Real photos vary: use sharp, well-lit, straight-on photos, one menu page or label per photo. The checks tell you which nutrition records to look at again.

How fast is it?

Measured on Apify at the default 1 GB: a page takes about 3 seconds for the AI to read, and pages of a PDF are read 4 at a time. The example input (a menu photo and a label) finishes in about 14 seconds including start-up; 20 single photos took about 70 seconds.

What it doesn't do

  • No translation: names and descriptions stay in the menu's language.
  • No guessing: allergens and dietary tags come only from what is printed (marks, legends, "contains" statements, bold ingredients). A dish without marks gets an empty list, which does not mean it is allergen-free. Always confirm allergens with the restaurant or the manufacturer.
  • No web pages or Google Maps: it reads image and PDF files, not restaurant websites or Maps listings. Link the menu photo or PDF itself.
  • No nutrition lookups: values come from the label, not from a food database.
  • Steep or very blurry photos may be misread: take the photo straight on.

Use it with AI agents (MCP)

https://mcp.apify.com?tools=bossless/menu-label-ocr

Ask your assistant "what are the vegetarian dishes on this menu and what do they cost?", and it runs this actor and answers from the rows.

Use the API from code

curl -X POST "https://api.apify.com/v2/acts/bossless~menu-label-ocr/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fileUrls": ["https://example.com/menus/lunch.jpg"]}'
from apify_client import ApifyClient
client = ApifyClient("YOUR_TOKEN")
run = client.actor("bossless/menu-label-ocr").call(run_input={
"fileUrls": ["https://example.com/menus/dinner.pdf", "https://example.com/labels/granola.jpg"],
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["recordType"] == "menu-item":
print(item["section"], "|", item["name"], "|", item["price"], item["currency"], item["allergens"])
elif item["recordType"] in ("nutrition", "product"):
print(item["productName"], item["off"], "failed checks:", item["checks"]["failed"])

FAQ

Do I need a Google, OpenAI or other API key? No. Everything is included in the price.

Which languages work? Menus and labels in most languages and scripts are read as printed. Nutrient names are recognised in English, German, French, Italian and Spanish for the Open Food Facts keys; in other languages the nutrients are still listed with their printed names.

Which currencies are recognised? Printed symbols and codes such as €, £, ¥, ₹, R$, zł, лв, Kč, CHF and every common 3-letter ISO code. A bare $ or kr is ambiguous, so it takes the Currency for unmarked prices you set, or stays empty.

Are the allergens safe to rely on? They are exactly what the menu or label prints, read by AI. Use them to sort and filter, and always confirm with the restaurant or the manufacturer before serving someone with an allergy.

What happens with a private or broken link? You get an error record saying why, and it isn't charged.

Is this affiliated with Open Food Facts, the FDA or any restaurant? No. It is not affiliated with any of them; it uses the open Open Food Facts field names so the data is easy to import.

Credits

Barcodes are read with zxing-cpp (Apache-2.0) and PDFs are rendered with PDFium via pypdfium2 (Apache-2.0 / BSD-3-Clause).