Skip to main content
Phantom is MealOps’ ordering infrastructure for AI agents. It gives your agent a single, safe interface to real restaurants: discovery, live menus, price quotes, payments, order submission, and tracking — with the restaurant’s own rules (hours, lead times, order caps) enforced server-side. This guide takes an agent team from zero to a first completed order. The full tool-by-tool contract lives in the companion document: MCP Tool Reference.

Endpoints

Phantom speaks MCP (Streamable HTTP) — the natural surface for AI agents: your client gets the tool list with schemas, relevance-ranked search envelopes, cross-restaurant dish search, the availability calendar, and server-published flow instructions on initialize.

Getting credentials

Access is by partnership. Email support@mealops.ai to set up a partner account. You receive:
  • A partner identity (your company) and one or more agent identities (a specific integration or app surface).
  • An API key per agent. The raw key is shown exactly once at creation — Phantom stores only a prefix and hash and can never show it again. Keys can be rotated (new raw key, old one stops working) and revoked instantly; every lifecycle action is audit-logged.
  • A set of scopes on the key (see below). Ask for the narrowest set you need; scopes can be upgraded later and apply on your next request with no reconnect.
Approval is platform-wide: MealOps vets and approves your agent once during onboarding. Once approved, your agent can discover and order from every live restaurant on the platform — there are no per-restaurant permissions to request. If an agent misbehaves, MealOps can suspend it platform-wide (orders are attributed to agents, and restaurants can report a problem agent to support). Operational guidance:
  • Use one key per integration surface; do not share a key across systems.
  • Rotate keys when integration owners change; revoke immediately on any suspected exposure.

Authentication

Send the key as a bearer token on every request:
The legacy header x-agent-api-key: <key> is also accepted and is treated identically.

Scopes

The MCP tool list is scope-filtered per credential. Tools your key cannot call are not registered at all — a discovery-only key (restaurants:read + menus:read) sees 7 tools instead of 17. Consequence: calling a tool your key can’t see returns the MCP “tool not found” error, not a scope message. If a tool seems missing from your list, check your key’s scopes first.

Connecting over MCP

Phantom speaks MCP over Streamable HTTP at POST /mcp. A typical client configuration:
On initialize the server publishes instructions — a short script of the canonical flow (discover → inspect → price → pay → submit → track) so agents sequence tools and pick the right payment branch without trial and error.

The canonical order flow

A complete order is about six calls:
  1. Discover — search_restaurants (or search_menu_items for “who has pad thai near me”). A location is required for restaurant results; a name query without a location returns brand-level summaries so you can ask the user “which area?”.
  2. Inspect — get_menu for the chosen restaurant. Item search results never carry modifier data; always read the full menu before building an order.
  3. Pick a time — get_availability_calendar returns the concrete windows an order may be scheduled inside (hours ∩ lead-time floor ∩ scheduling horizon). Use it instead of guess-and-check validate loops.
  4. Validate — validate_order runs the restaurant’s full rule set and returns machine-readable issue codes.
  5. Price — quote_order returns the authoritative total (subtotal, tax, fees, courier fee, tip).
  6. Pay — start_payment (see payment branches below). Every order is prepaid: payment is charged or authorized before submit, always.
  7. Submit — submit_order creates the order and sends it to the restaurant’s POS (orders auto-approve; there is no manager review step).
  8. Track — get_order_status / get_order_tracking.

The ordering-time model

Several tools return timing fields. They are components of one formula — do not invent your own:
transit is the restaurant’s own deliveryTransitEstimateMinutes (seeded from the POS provider’s average delivery time when available); unknown transit contributes nothing to the floor — show a wider ETA range instead of inventing a number. The formula sets the earliest schedulable time; the latest is the platform scheduling horizon: orders cannot be scheduled more than 7 days ahead — validation rejects later times with lead_time_too_long, and get_availability_calendar never returns windows beyond it. update_order applies the identical gate to a rescheduled order, so a time you could not have submitted is not reachable by editing afterwards either — and only up until the restaurant accepts, after which the time is fixed entirely (see Visibility and ownership rules). Timezones — read this before scheduling anything. All times on the wire are ISO-8601 instants. Every tool that takes a time accepts a UTC Z instant or an explicit offset (2026-08-11T12:00:00-07:00) and normalizes it to UTC, so everything you read back is Z. Never send a bare local wall clock with no zone. A scheduled time is restaurant-local in meaning: opening hours, per-item availability, and get_availability_calendar are all judged in the restaurant’s own IANA timezone (get_restaurant_detail.restaurant.timezone). So when a user says “7pm”, resolve that against the restaurant’s zone, not the end user’s device zone — the two produce different instants, and only one of them is 7pm to the kitchen. To make that checkable, validate_order and submit_order echo the resolved time back:
restaurantLocal is the wall clock the restaurant will read. Confirm it matches what the user asked for before you submit — an off-by-a-timezone is otherwise invisible until the order lands in the kitchen at the wrong hour. Scheduled fulfillment times are restaurant-local in meaning — use the restaurant’s timezone to render them to users. For self-delivery orders the pickup and delivery times are the same instant; the 15-minute courier collection lead applies only when a real courier collects.

The order intent

validate_order, quote_order, start_payment, and submit_order all take the same { restaurant_id, order } shape:
Rules:
  • restaurant_id at the tool level and inside order must match, or the call throws.
  • order.agent_id is optional — leave it out, as every example here does. Phantom stamps the authenticated agent from your API key; you have no way to know your own Phantom agent id before your first call, and you never need it. Supplying a different one throws.
  • fulfillment_address is required for delivery. Send latitude/longitude when your address autocomplete has geometry — courier dispatch needs them.
  • Per-person naming: an item line with player_first_name must have quantity: 1 — two named people means two item lines.
  • fulfillment_type is one of pickup | delivery | eat_in | curbside.

Validation issue codes

validate_order returns { valid, issues: [{ code, message, field, severity }] }. Codes currently emitted: ordering_disabled, channel_deactivated, restaurant_closed_at_time, location_paused, lead_time_too_short, lead_time_too_long, headcount_too_large, fulfillment_type_not_allowed, self_delivery_not_enabled, menu_version_stale, player_name_incomplete, player_name_requires_separate_item_line, item_not_found, item_unavailable, item_unavailable_at_time, mapping_needs_review, modifier_group_invalid, modifier_invalid, modifier_unavailable, nested_modifier_quantity_invalid, modifier_not_nested, required_modifier_missing, too_many_modifiers, modifier_quantity_exceeded, too_many_items, order_value_too_large. Some codes are error codes, not issue codes — they arrive as a thrown tool error, never as an entry in issues[]: delivery_unavailable (quote), order_validation_failed (submit / start_payment), payment_required and payment_amount_insufficient (submit, and the latter also on a re-pricing update_order), payment_reference_conflict (start_payment), and order_already_accepted (update_order). The full list is in the ordering reference.

Payments

All orders are prepaid — there is no invoicing or pay-on-delivery path. This is enforced server-side: submit_order rejects with a payment_required error unless the order’s payment (matched by external_order_reference) is already charged or authorized. Phantom is charged-first merchant of record: the eater’s card is charged (or authorized) before submit, and the restaurant is paid out on acceptance. start_payment picks its branch from what you supply: The payment must cover the order, not merely exist. submit_order compares the authorized amount to the order total and rejects a short one with payment_amount_insufficient. Since payments are keyed by external_order_reference and most clients keep that reference stable across cart edits, this is the case you will actually hit: if the cart changed after you started payment, call start_payment again with the current order — it releases the previous hold and authorizes the new total under the same reference. An over-authorization is accepted and capture takes only the order total. Supporting tools: void_payment (release a held authorization when a submit fails) and get_payment_status (reconciliation by your external_order_reference). Capture and the restaurant payout run automatically when the restaurant accepts the order — no agent action settles money.

Rate limits

All quotas are per agent on a fixed 60-second window — every credential your agent holds, and both auth headers, share one bucket. Responses carry x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (epoch ms); a 429 adds retry-after (seconds). Treat 429 as retryable after a short backoff. The MCP endpoint returns rate-limit errors as JSON-RPC errors (error.code: -32000), matching every other error it returns; the REST routes return { "error": "…" }. MCP (POST /mcp): The two per-tool caps have independent budgets: spending all 60 dish searches leaves your 60 delivery probes intact, and neither draws down the other. Both are per agent on their own 60-second window, and both reject before doing any work, so a throttled call costs nothing but the error. The limits above are the ones you’ll encounter in normal integration work. Additional undisclosed abuse-prevention limits also apply platform-wide; a well-behaved agent operating within the published limits will never hit them.

Visibility and ownership rules

  • A restaurant is omitted from your results entirely when agent ordering is off, the restaurant is not payout-ready, or its POS channel is deactivated (re-activation restores it automatically).
  • Order tools enforce ownership: your agent can only read, track, cancel, or update orders it created. Foreign and missing order ids both return Order <id> not found. — ids are not enumerable across agents.
  • cancel_order works only before the restaurant accepts. It cancels the POS ticket and any courier, and best-effort reverses payment (refund with payout clawback, or void of a held authorization).
  • update_order can change fulfillment time, dropoff address, and dropoff notes on a live order. Menu items are not editable post-submit — the restaurant’s POS ticket cannot be amended; cancel and reorder instead. Two boundaries follow from that same fact:
    • Time changes stop at acceptance (order_already_accepted). The kitchen’s ticket keeps the original time, so moving it afterwards would only re-point the courier and leave the food waiting. Dropoff address and notes stay editable — the kitchen never sees them.
    • Delivery edits re-price the order. Changing a delivery order’s dropoff or time re-quotes the courier fee before anything moves; the new total becomes the order total and capture follows it. If it no longer fits inside the payment you already placed, the update is rejected with payment_amount_insufficient and nothing changes.

Best practices

  • Prefer server-resolved values over recomputing: resolvedMinLeadTimeMinutes, earliest_orderable_time, the quote total, and check_delivery_availability are authoritative.
  • Use get_availability_calendar to pick fulfillment times instead of looping validate_order.
  • Use the fields projection and pagination (offset, total_matches, next_offset) on search_restaurants for cheap disambiguation before pulling full entries.
  • When a search returns empty, read no_results_reason and recover (location_required → ask for a location; all_closed_at_requested_time → offer a later time) instead of guessing.
  • Respect menu_version_stale: re-read the menu and rebuild the order if validation says your menu snapshot is out of date.
  • submit_order never bypasses the restaurant’s ordering rules — opening hours, busy-mode pause, lead times, and caps are enforced server-side. Orders auto-approve; track status for POS acceptance.

Support

Partnership, credentials, scope changes, and rate-limit increases: support@mealops.ai.