All posts
Tutorial6 min read

ORA field guide: Menu

Mert Iseri

This is a field guide for two ORA fields that ship together: menu, the structured menu, and menu_items, the flat list of item names. Each field guide answers the same five questions: what the field means, what the agent gets, how the API works for this one field, what is actually being scraped, and how to reproduce it. You need an API key (get one self-serve). Every response shown is a real captured API response.

What the fields mean

menu is the restaurant's publicly published menu as a structured object: sections, each with items carrying name, price_text, and description, plus counts and two capture-quality signals, menu_capture_status and menu_completeness_confidence. menu_items is the same menu flattened to a list of item names, which is the shape you want for quick filtering ("which of these 200 restaurants sell deep dish").

What counts: what the restaurant actually publishes on its public surfaces, its own website, its ordering pages, or its public marketplace listings. Prices come back as price_text strings exactly as printed, not parsed numbers. What to watch for: a menu captured from a delivery listing can carry delivery pricing and be a subset of the dine-in menu. ORA tells you this about its own answer instead of hiding it: our capture below reports menu_capture_status: "partial" with a completeness confidence of 0.6. Read those two keys before treating any menu as complete.

Both fields come from the same workflow (source_workflow: "menu-extraction-v2"), so requesting both in one job costs one job, not two.

What the agent gets

The real result for Lou Malnati's Pizzeria in River North: 197 items across 52 sections. The full raw response for this job is 169 KB; this excerpt is truncated where marked with ...:

{
  "job_id": "bdef94c3-afea-4433-83ae-c24b4abaf020",
  "status": "completed",
  "name": "Lou Malnati's Pizzeria",
  "address": "439 N Wells St, Chicago, IL 60654",
  "website": "https://www.loumalnatis.com/chicago-river-north",
  "google_place_id": "ChIJNUoDqLYsDogRG8jygr_J23U",
  "fields": {
    "menu": {
      "status": "completed",
      "value": {
        ...
        "sections": [
          ...
          {
            "name": "Deep Dish Malnati Chicago Classic™",
            "items": [
              {
                "name": "Personal - Deep Dish Chicago Classic",
                "price_text": "$18.42",
                "description": "Made with Lou's lean sausage, some extra mozzarella cheese and vine-ripened tomato sauce on Buttercrust™. It's authentic Chicago!"
              },
              {
                "name": "Small - Deep Dish Chicago Classic",
                "price_text": "$29.10",
                "description": "Made with Lou's lean sausage, some extra mozzarella cheese and vine-ripened tomato sauce on Buttercrust™. It's authentic Chicago!"
              },
              ...
            ]
          },
          ...
        ],
        ...
        "item_count": 197,
        ...
        "section_count": 52,
        ...
        "menu_capture_status": "partial",
        ...
        "menu_completeness_confidence": 0.6
      },
      "evidence_url": "https://www.ubereats.com/store/lou-malnatis-pizzeria-river-north/IeR4jOAcVACWkRDXwbOsdA",
      "source_workflow": "menu-extraction-v2",
      "error": null
    },
    "menu_items": {
      "status": "completed",
      "value": [
        "Small - Thin Crust Cheese",
        "Personal - Deep Dish Cheese",
        "Personal - Deep Dish BBQ Chicken",
        ...
      ],
      "reasoning": "Extracted 197 menu item names from Schema.org JSON-LD menu.",
      "evidence_url": "https://www.ubereats.com/store/lou-malnatis-pizzeria-river-north/IeR4jOAcVACWkRDXwbOsdA",
      "source_workflow": "menu-extraction-v2",
      "error": null
    }
  },
  ...
}

Notice what the response admits about itself. The evidence_url is the restaurant's public Uber Eats store page, and menu_capture_status: "partial" plus a 0.6 completeness confidence tell you this is the delivery menu, priced for delivery, not a sworn copy of the dine-in menu. An agent that reads those two keys knows exactly how much weight the data can bear.

How the API works for this one field

One job, both menu fields. Create it (this is the only step that spends a credit, 1 per accepted job):

API_KEY=$(cat ~/.cache/248/ora_api_key)
curl -sS -X POST "https://ora.248.ai/v1/enrichment/jobs" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -d '{
    "name": "Lou Malnati'\''s Pizzeria",
    "address": "439 N Wells St, Chicago, IL 60654",
    "fields": ["menu", "menu_items"],
    "idempotency_key": "blog-2026-07-22-lou-malnatis-menu"
  }'

The real create response:

{
  "job_id": "bdef94c3-afea-4433-83ae-c24b4abaf020",
  "status": "pending",
  "requested_fields": [
    "menu",
    "menu_items"
  ],
  "production_run_id": "bdef94c3-afea-4433-83ae-c24b4abaf020",
  "created_at": "2026-07-22T23:23:40.555558Z"
}

Poll the status (free) every 10 seconds until it is completed, failed, or cancelled, then fetch the result (free):

curl -sS "https://ora.248.ai/v1/enrichment/jobs/bdef94c3-afea-4433-83ae-c24b4abaf020" \
  -H "X-API-Key: $API_KEY"
curl -sS "https://ora.248.ai/v1/enrichment/jobs/bdef94c3-afea-4433-83ae-c24b4abaf020/result" \
  -H "X-API-Key: $API_KEY"

This run took 169 seconds as a fresh enrichment, the slowest of the fields we captured for this series, which tracks with menus being the largest payloads. Save the raw response to disk before flattening anything; at 169 KB it is exactly the kind of response you do not want to re-fetch from memory later. Inputs are name and address only. If the name does not match the Google listing at that address, the fields come back skipped with "error": "google_match_unmatched"; send the name as Google lists it and retry.

What is actually being scraped

Only public surfaces, and the response names them. In this capture:

  • The restaurant's public Uber Eats store page (the evidence_url on both fields).
  • The structured menu data published on that page: the menu_items reasoning says outright that the 197 names came from the page's Schema.org JSON-LD markup.

Which public surface backs a given restaurant's menu varies: it can be the restaurant's own site or an ordering page rather than a marketplace listing. You do not have to guess; the evidence_url on the returned field is the page the menu came from, and you can open it.

How another agent reproduces it

Paste this into Claude Code or Codex. It works in any agent with shell access:

Pull the menu for one restaurant with the ORA API. Host https://ora.248.ai. Auth header X-API-Key on every request; load the key with API_KEY=$(cat ~/.cache/248/ora_api_key) and never print, echo, or commit it. Account and credits endpoints use the /api/v1 prefix; enrichment endpoints use /v1. Free reads: GET /api/v1/account/credits, GET /v1/enrichment/fields, job status, and results. Spending: POST /v1/enrichment/jobs costs 1 credit per accepted job, so create exactly one job.

POST /v1/enrichment/jobs with {"name": "<name as it appears on Google>", "address": "<street address>", "fields": ["menu", "menu_items"], "idempotency_key": "menu-<slug of name and address>"}. Reuse the same idempotency_key on any retry; it returns the existing job instead of billing a new one. Poll GET /v1/enrichment/jobs/{job_id} every 10 seconds until status is completed, failed, or cancelled; a fresh run takes roughly one to three minutes and menu payloads can be large. Then GET /v1/enrichment/jobs/{job_id}/result and save the full response to a file unmodified before extracting anything.

Report from fields.menu.value: item_count, section_count, menu_capture_status, menu_completeness_confidence, and the section names; and from fields.menu (top level of the field): evidence_url. If menu_capture_status is "partial", say so prominently; the menu may be a delivery-priced subset. Use fields.menu_items.value when I only need item names. If status is skipped with error google_match_unmatched, the name did not match the Google listing at that address; tell me instead of retrying variations, since each accepted job costs a credit.

More field guides: POS provider and Gift cards. For the CSV-scale version of this flow, see the tool guides for Claude Code and Codex.