> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mealops.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Onboarding

> Take your agent from zero to a first completed order on Phantom: credentials, scopes, endpoints, the canonical flow, timing, payments, and rate limits.

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, approval flow) 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](/reference/overview).

## Endpoints

| Surface                            | URL                                                |
| ---------------------------------- | -------------------------------------------------- |
| MCP (Streamable HTTP) — production | `https://phantom.mealops.ai/mcp`                   |
| MCP (Streamable HTTP) — staging    | `https://staging.phantom.mealops.ai/mcp`           |
| REST — production                  | `https://phantom.mealops.ai/api/agent/...`         |
| REST — staging                     | `https://staging.phantom.mealops.ai/api/agent/...` |

MCP is the recommended surface: it is a thin wrapper over the same REST services (no separate ordering logic), but adds relevance-ranked search envelopes, cross-restaurant dish search, the availability calendar, and server-published flow instructions. Everything your key can do over MCP it can also do over REST, minus the MCP-only discovery tools.

## Getting credentials

Access is by partnership. Email [support@mealops.ai](mailto: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 (MCP and REST):

```
Authorization: Bearer <your-agent-api-key>
```

The legacy header `x-agent-api-key: <key>` is also accepted and is treated identically.

## Scopes

| Scope              | Grants                                                                                        |
| ------------------ | --------------------------------------------------------------------------------------------- |
| `restaurants:read` | Restaurant discovery, detail, availability calendar, brand expansion, delivery probe          |
| `menus:read`       | Full menus and cross-restaurant dish search                                                   |
| `orders:validate`  | Rule validation of a draft order                                                              |
| `orders:quote`     | Pricing quotes                                                                                |
| `payments:start`   | Start/charge/void payments, payment status                                                    |
| `orders:submit`    | Submit, cancel, and update orders (implies `payments:start` — submitting already moves money) |
| `orders:status`    | Order status and tracking                                                                     |

**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 20. 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:

```json theme={null}
{
  "mcpServers": {
    "phantom": {
      "type": "http",
      "url": "https://phantom.mealops.ai/mcp",
      "headers": {
        "Authorization": "Bearer <your-agent-api-key>"
      }
    }
  }
}
```

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.

A REST equivalent for quick checks:

```bash theme={null}
curl -s "https://phantom.mealops.ai/api/agent/restaurants?postal_code=94063" \
  -H "Authorization: Bearer <your-agent-api-key>"
```

## 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) when the restaurant requires prepaid checkout.
7. **Submit** — `submit_order` creates the order and enters the restaurant's approval flow.
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:

```
pickup-like:  max(prep + busyDelay, notice, 15)
delivery:     max(prep + busyDelay + 25 transit, courierFloor, notice)
```

| Field                        | Meaning                                                                                                                                                                                                                                           |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prepTimeMinutes`            | How long the kitchen needs. Operator-configurable; `0` = not published.                                                                                                                                                                           |
| `minimumLeadTimeMinutes`     | Required ordering **notice**, distinct from prep.                                                                                                                                                                                                 |
| `deliveryMinLeadTimeMinutes` | Courier-imposed floor for **delivery only** (the courier network requires \~95 minutes advance). `null` when the restaurant drives its own orders (`selfDeliversEnabled`) or courier delivery is off — then prep is the only real constraint.     |
| `deliveryCourier`            | `"deliverthat"` or `null` (self-delivery / courier disabled).                                                                                                                                                                                     |
| `busyState`                  | `{ status, delayMinutes, receivedAt }` or `null`. Status `busy` adds `delayMinutes` to prep for new orders; `paused` means the store is not accepting near-term orders — validation rejects orders due within 120 minutes with `location_paused`. |
| `resolvedMinLeadTimeMinutes` | `{ pickup, delivery }` — the server-computed result of the formula, including the current busy delay. **Prefer this**; recompute from components only if it is absent.                                                                            |

The 25-minute delivery transit allowance is currently a flat constant, not per-restaurant.

**Timezones:** all times on the wire are ISO-8601 UTC. 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:

```json theme={null}
{
  "restaurant_id": "rest_lb_steakhouse",
  "order": {
    "restaurant_id": "rest_lb_steakhouse",
    "external_order_reference": "yourapp-order-1001",
    "customer": { "name": "Avery", "email": "avery@example.com", "phone": "+16505551234", "teamName": "U14 Blue" },
    "fulfillment_type": "pickup",
    "requested_fulfillment_time": "2026-08-11T19:00:00.000Z",
    "fulfillment_address": {
      "address1": "500 Broadway",
      "city": "Redwood City",
      "state": "CA",
      "postal_code": "94063",
      "notes": "Gate code 1234",
      "latitude": 37.4852,
      "longitude": -122.2364
    },
    "headcount": 2,
    "tip_cents": 500,
    "budget_constraints": { "max_total_cents": 20000 },
    "payment_policy": "required_before_submit | invoice_manual | stored_payment",
    "items": [
      {
        "item_id": "item_filet",
        "quantity": 1,
        "player_first_name": "Avery",
        "player_last_name": "Nguyen",
        "notes": "No butter",
        "modifiers": []
      }
    ],
    "dietary_constraints": [],
    "packaging_instructions": "Label each box",
    "include_cutlery": false,
    "substitution_policy": "strict | allow_equivalent | require_approval",
    "metadata": {}
  }
}
```

Rules:

* `restaurant_id` at the tool level and inside `order` must match, or the call throws.
* `order.agent_id` is stamped with the authenticated agent automatically; 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 | catering | eat_in | curbside`.

## Validation issue codes

`validate_order` returns `{ valid, issues: [{ code, message, field, severity }] }`. Codes currently emitted:

`ordering_disabled`, `channel_deactivated`, `location_paused`, `lead_time_too_short`, `lead_time_too_long`, `headcount_too_large`, `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`, `delivery_unavailable`, `order_validation_failed`.

## Payments

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:

| Supplied                            | Branch            | Behavior                                             |
| ----------------------------------- | ----------------- | ---------------------------------------------------- |
| `customer_id` + `payment_method_id` | `saved_method`    | Charge a saved card                                  |
| `payment_token`                     | `shared_token`    | Confirm a Stripe-shareable token                     |
| neither                             | `hosted_redirect` | Hosted Stripe Checkout; saves the card for next time |

Supporting tools: `charge_saved_card` (pre-submit charge), `charge_payment` (charge for an already-submitted order), `void_payment` (release a held authorization when a submit fails), `get_payment_status` (reconciliation by your `external_order_reference`), `create_payout` (reconcile fallback — payout normally runs automatically when the restaurant accepts).

## Rate limits

All limits are per credential on a fixed 60-second window. Exceeding one returns HTTP 429 with `{ "error": "…" }`; every response carries `x-ratelimit-remaining` and `x-ratelimit-reset` (epoch ms). Treat 429 as retryable after a short backoff — `x-ratelimit-reset` gives the exact boundary.

MCP (`POST /mcp`):

| Layer                 | Default           | Notes                                                                                                                             |
| --------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Per-agent quota       | **300 calls/min** | The governing limit; adjustable per agent — ask if you need more. \~50 complete order flows per minute.                           |
| Anti-abuse IP ceiling | 600/min           | Pre-auth only; does not cap an authenticated agent. High-volume partners egressing through few IPs should tell us so we raise it. |
| `search_menu_items`   | 60/min            | Separate, stricter — each call can scan up to 25 menus.                                                                           |

REST (`/api/agent/*`), per route:

| Route                                                                                             | Limit / min |
| ------------------------------------------------------------------------------------------------- | ----------- |
| `GET /restaurants`, `GET /restaurants/:id`, `GET /restaurants/:id/menu`, `GET /orders/:id/status` | 120 each    |
| `POST .../orders/validate`, `POST .../orders/quote`                                               | 60 each     |
| `POST .../orders/submit`                                                                          | 30          |

## 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.

## 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 approval rules; an order may come back `needs_approval` and be accepted or declined by a human. Track it.

## Support

Partnership, credentials, scope changes, and rate-limit increases: [support@mealops.ai](mailto:support@mealops.ai).
