# Ordering over MCP

MCP is one of two doors to the same ordering; [REST carts](https://developer.layout.link/reference/carts) is the other, with the same checks behind both. Mint a build session for a provisioned user, point an MCP client at our server, and drive the `order` tool. The session builds and reads a cart, and confirms it once the person says yes, with the 6-digit code Layout texts them when [a code is due](https://developer.layout.link/reference/how-it-works#when-a-code-is-due). In sandbox a cart is built from the restaurant's menu as Layout holds it, with no browser, and the confirm is always simulated; it takes no code unless the cart was built to ask for one. A person with their own Layout account can instead connect through an OAuth client you created, and confirm from your app.

## Connect

Mint a build session with `POST /v1/users/:id/grant` and send its `lgb_` token as a bearer credential. The grant authenticates the session for that one provisioned user, with no OAuth flow. A secret key sent to `mcp.layout.link` is refused with `401`. An expired build grant answers `401` `grant_expired` on both hosts, and its `WWW-Authenticate` carries `error_description="grant_expired"`. Mint a new grant. Any other refused grant answers `unauthorized`. Every answer that comes before the call is on [MCP tools](https://developer.layout.link/reference/mcp-tools#before-the-call).

When the person has their own Layout account and signs in themselves, connect with an [OAuth client you created](https://developer.layout.link/reference/authentication#oauth-clients) instead. The MCP client starts the standard authorization flow at `https://mcp.layout.link`, whose metadata names `https://api.layout.link` as the authorization server, and the session acts as that person, who approved your app on Layout's consent screen.

```json
{
  "mcpServers": {
    "layout": {
      "url": "https://mcp.layout.link",
      "headers": { "Authorization": "Bearer lgb_9f3a2c…" }
    }
  }
}
```

## The tools

A build session has three tools: `order`, `order_status` and `places`. A sandbox build session also has `menu`. `order` is a state machine rather than separate calls: `action` walks it through `preview`, `build`, `status` and `confirm`. An OAuth connection acts as the person, so it gets every Layout tool, including `menu` and `item_photo`.

| Tool | What it does |
| --- | --- |
| `order` | Resolve a restaurant, build a cart and confirm it. Driven by `action`, below. |
| `order_status` | Long-poll a build (by `idempotencyKey`) or a placement (by `orderId`, which also takes the `ord_` id) until it settles. Read-only and free. A ready cart reads `placement.status: "carted"`: stop polling, it waits on the person. A placed order carries `placement.pickup`. |
| `places` | Find restaurants by name or description, from the restaurants Layout holds. It never opens a restaurant's site. In sandbox it lists only restaurants whose menu Layout holds, and an empty list it emptied for that reason carries `emptyBecause: "no_held_menu"`: search another restaurant, kind of food or area. |
| `menu` | Sandbox build sessions only. The menu Layout holds for a place, the one a sandbox cart there is priced from. It never reads a menu live, costs nothing, and can be called whenever you need to check what a place sells. `mode: "knowledge"` answers the same way. A place with none answers `status: "unreadable"` with `code: "menu_not_held"`. |

Every tool's full input and output JSON Schema, and the raw JSON-RPC calls, are on [MCP tools](https://developer.layout.link/reference/mcp-tools). How each tool, state and id maps to REST is in [Two doors, one order](https://developer.layout.link/reference/how-it-works#two-doors).

An OAuth connection also has `item_photo`: what one menu item looks like, from the restaurant's own published photo. It answers `photo` with an `imageUrl`, `pending` while Layout is still looking (ask again shortly), or why there is none. It is the same lookup as [POST /v1/images/items](https://developer.layout.link/reference/api#post-v1-images-items), which a build session can use over REST.

### The order actions

| action | What it does |
| --- | --- |
| `preview` | Resolve the restaurant and estimate. Pass the restaurant's name in `restaurant` (or in `query`), not the food. A name Layout cannot find answers `store_not_found` either way. |
| `build` | Start building the cart. Holds for up to about 15 seconds: a build that finishes inside that window comes back with its result, `carted` when the cart is ready. Otherwise it returns `building` and you poll `status` for the rest. A build session orders pickup only: a build with `fulfillment: "delivery"`, `deliverTo` or `tipPercent` builds nothing and answers `deliveryUnavailable: { reason: "not_enabled" }`. |
| `status` | Poll a build by `idempotencyKey`, or a placement by `orderId`. |
| `confirm` | Place the cart the person approved. On a production build session it needs the code Layout texts the person when a code is due: the call without one then texts it and answers `confirm.error: "code_required"`. When none is due it places. On a sandbox build session for a provisioned test user it is always simulated. On an OAuth connection it places a real order, passing the code Layout texted the person in `code` when the build's `confirmation.method` is `sms_code`; for a [sign-in test account](https://developer.layout.link/reference/authentication#sign-in-test-accounts) it is simulated and answers `simulated: true`. |

A `build` that carries a `placeId` and a `restaurant` or `restaurantName` naming a different store builds nothing and answers `storeMismatch`: `store` is the store the `placeId` names, with its address, and `candidates` are stores matching the name when Layout could find them. Layout compares every word of the name you send against the store's name and address, as `POST /v1/carts` does. Ask the person which one they mean and build again with that store's `placeId`, its name as `restaurant`, a `near`, the order in `query` and a fresh `idempotencyKey`. When a build asks which branch (`needsLocation`), build with the chosen option's `placeId` and its own name as `restaurant`. A build needs `query` and a store to build on: `platform`, an https `orderUrl`, or a restaurant name in `restaurant` or `restaurantName` with `near` or `geo`. Pass the `placeId` too when you have it. A `placeId` alone is refused as `invalid_input`.

When the person says no, call `order` with `action: "status"`, `cancel: true` and the cart's `orderId` to release it. The schema requires `action`. A cart still building has no `orderId` yet: cancel it with that build's `idempotencyKey`. The answer is `cancel.status: "canceling"`, Layout releases the cart the moment the build lands and never texts the person about it, and a later cancel answers `canceled`. A build session can cancel by key only a build its own application started; for any other key the answer is `no_active_order`. A cancel on a build session naming neither an `orderId` nor an `idempotencyKey` is refused as `forbidden`. A cart already confirmed answers `already_placing` or `placed`, and is not canceled.

### What a preview concluded

A `preview` result carries `preview.outcome`, one word for what Layout found. Branch on it rather than working it out from the other fields.

| outcome | What it means |
| --- | --- |
| `ready` | A store resolved and nothing stands in the way. Call `build` next. |
| `needs_location` | Layout needs to know which branch. Offer the options in `preview.nearby` and preview again with the chosen `placeId`. |
| `closed` | The store is closed right now. Relay `preview.sayToUser`; it names any open alternative. |
| `closing_soon` | Open, but too close to closing for Layout to take the order. |
| `not_orderable` | The restaurant takes no online orders Layout can place. |
| `blocked` | Layout does not place orders at this restaurant. |
| `store_not_found` | Layout found no restaurant by that name. `preview.storeNotFound.candidates` are restaurants search found under other names, within about 30 miles; offer them by their own names, never as branches, and build at one only if the person picks it, with its `placeId` and its own name as `restaurant`. A build with a name and no `placeId` answers the same way. |

When the person names a street and exactly one open branch sits on it, the preview resolves that branch instead of asking which one, and the confirmation card still names the store before anything is bought.

### Confirm with the person's code

On a build session, a person who has not yet agreed to your application ordering for them is asked for a code on their first order (`first_order_consent`), and that code is the agreement. When they have already agreed, and which other confirms ask for a code, is in [When a code is due](https://developer.layout.link/reference/how-it-works#when-a-code-is-due). Call confirm with no code either way; it answers `placing`, or `code_required` when a code is due. When the person is not ready to confirm, it answers `build_only` with `confirm.next.step` (`verify_required`, `connect_required`, `phone_required` or `card_required`), and texts nothing. On `card_required`, mint a [card link](https://developer.layout.link/reference/card-links) and send it to them. A build session gets no `confirmationCard`: tell the person the items, the store and `price`, the build's `totalMinor`, which includes Layout's service fee, and wait for their yes. `cart.merchantTotalMinor` is the restaurant's own total.

A confirm that goes ahead answers in `confirm.status`: `placing`, `code_sent` after a resend, `code_not_needed`, `already_placing`, `placed`, `not_confirmable_cold`, or `session_gone` when the cart's checkout session ended and it must be built again. `not_confirmable_cold` carries `reason`: `canceled`, `expired`, `closed` or `not_found`. It can also answer `failed`, with a `code`, when nothing was placed; `confirm_expired` or `not_placed`, when nothing was placed and the cart must be built again; `confirmed`; or `place_error`. A confirm on a carted build more than fifteen minutes after it was ready answers `confirm_expired`, and nothing is charged. On `place_error`, relay `userMessage`, follow `assistantInstruction`, and poll `status` with the `orderId`: the outcome can be unknown. A confirm Layout refuses is still an ordinary tool result, not `isError`: `confirm.status` is `"error"` and `confirm.error` names the reason, so branch on `confirm.error`. `code_required`, `build_only`, `price_changed` and `send_failed` all arrive this way.

1. Call `confirm` with the `orderId`, the carted build's `totalMinor` (fee included) as `expectedTotalMinor`, a fresh `idempotencyKey` and no `code`. When a code is due, the answer is `confirm.error: "code_required"`, and Layout texts the person a code that names the total, the store and the card, in Layout's words. `codeSent: true` means it just went out. `codeSent: false` means a code Layout texted for this cart in the last 10 minutes is still good and nothing new was sent: ask the person for that one. A text that could not be sent answers `send_failed`: retry shortly. `texts_blocked` means the person's number replied STOP to Layout and nothing was texted.
2. Ask the person for the code.
3. Call the same `confirm` again, with the same `idempotencyKey`, plus `code`. The answer is `placing`; poll `status` with the `orderId` until it is `placed`, `failed` or `unconfirmed`.

The code works once, for this cart and this total, for 10 minutes. `stage: "resend"` texts a fresh one, which replaces the last, and answers `code_sent`; when no code is due it answers `code_not_needed` and texts nothing. Layout texts at most four codes per cart: past that, or past your application's daily code allowance, the confirm answers `rate_limited` with a `reason`. A wrong code answers `bad_code`, and a code that no longer fits answers `expired`, `amount_mismatch` or `no_pending_auth`. On `too_many_attempts`, wait `retryAfterSec`, ask the person for the code again and confirm with it. On `limiter_unavailable`, when Layout could not count the tries, or a `rate_limited` with `retryAfterSec`, wait that long and send the same call again. A session can confirm only carts it built. Never invent a code, and never ask the person for a Layout sign-in code: an order code only approves the one cart it names.

Other refusals carry the same `confirm.error` shape. These confirm and charge nothing: `price_changed` (the total no longer matches: build a fresh cart with the same request and a new `idempotencyKey`, which carries the moved total, tell the person, and confirm with it), `over_daily_cap` (the account's daily spending limit), `risk_denied`, `idempotency_conflict` (the key was used for a different request; retry with a fresh one) and `unavailable` (a check could not run; retry the same confirm shortly). `in_progress` means a confirm with this `idempotencyKey` is still running: poll `status` with the `orderId`, and never confirm again. `confirm_failed` can come after the order was handed off: Layout lost track of the confirm and the order may be placing. Follow `assistantInstruction`, poll `status` with the `orderId`, and never confirm again.

A build session's confirm that sends the restaurant's total alone, without Layout's service fee, answers `price_changed` with `reason: "fee_not_included"`. The price did not move: confirm with the build's `totalMinor`, the total the person was shown.

### Build in sandbox

A sandbox session builds at the restaurants its `places` tool lists, which in sandbox are only those whose menu Layout holds. A `build` also takes `simulate` (the ending), `latency` (`realistic` or `instant`) and `engine` (`shadow` or `real`), exactly as [Sandbox](https://developer.layout.link/reference/sandbox) describes. A production session that sends any of them is refused with `sandbox_only`. A restaurant whose menu Layout does not hold, or a build that names no place Layout can tie to a `placeId`, answers a build refusal with `refusal.code: "menu_not_held"`: pick another place from `places`. A cart that fails `ITEM_UNAVAILABLE` named something the menu does not list, or lists as sold out: call `menu` with that `placeId` and build again with names it lists. A cart that answers `needs_choices`: ask the person, then build again with the same `query` and store, a new `idempotencyKey`, and `choices` set to every answer given so far, joined with `"; "`. How the request is matched to the menu is in [How a sandbox cart is built](https://developer.layout.link/reference/sandbox#how-a-sandbox-cart-is-built). The tools a sandbox session lists, and its instructions, are in [MCP tools](https://developer.layout.link/reference/mcp-tools).

### Confirm in sandbox

Pass the `orderId` from the carted build, its `totalMinor`, which includes Layout's service fee, as `expectedTotalMinor`, and a fresh `idempotencyKey`. No code is needed, except on a cart built with `"simulate": "code"` or at `test_place_code`: its confirm answers `confirm.error: "code_required"` with `codeSent: false` until it carries `"code": "000000"`, and nothing is texted. Any other code answers `bad_code`.

```
order {
  "action": "confirm",
  "orderId": "0b6f2a54-…",
  "expectedTotalMinor": 1949,
  "idempotencyKey": "a7c1e0d2-…"
}

→
{
  "action": "confirm",
  "confirm": {
    "status": "placed",
    "orderId": "0b6f2a54-…",
    "totalMinor": 1949,
    "merchantTotalMinor": 1850,
    "subtotalMinor": 1701,
    "simulated": true,
    "userMessage": "This is a test account, so that was a simulated order. No money moved, no card was charged, and no restaurant received it."
  },
  "publicOrderId": "ord_7c21e4b9a0d3"
}
```

A build session's confirm carries three amounts: `totalMinor`, the total you confirmed with, fee included, on every confirm about an order that went ahead, including `already_placing` and an `alreadyConfirmed` answer, where it is the order's own total; `merchantTotalMinor`, the restaurant's own total; and `subtotalMinor`, the food before tax, absent when Layout cannot read it. `simulated: true` means no restaurant received the order and nothing was charged. Never describe it as a real order. A sandbox cart at a real restaurant, and `test_place_placed`, `test_place_failed`, `test_place_unconfirmed`, `test_place_challenge`, `test_place_code` and `test_place_pickup_later`, answer the confirm with `status: "placing"` and `simulated: true`: poll `status` with the `orderId` for the ending. Every other test restaurant answers `placed` in the same call, as above. A total that no longer matches the cart is refused with `price_changed`, just as in production. A simulated order still sends an `order.placed` webhook, and its `order` object carries `"simulated": true`, as does `GET /v1/orders/:id`. A real order never carries that key. A `resend` stage on a sandbox session texts nothing and places nothing: it answers `code_not_needed`, or `code_sent` on a cart built with `"simulate": "code"` or at `test_place_code`; nothing is texted, and the code (`000000`) is not in `userMessage`. A sandbox session releases a cart it built the same way as production, with `action: "status"`, `cancel: true` and the `orderId`, or the build's `idempotencyKey` while it builds. A `preview` with a `test_place_` `placeId`, or a `restaurant` naming a Layout Test Kitchen store, resolves that [test restaurant](https://developer.layout.link/reference/sandbox).

## Relay the card verbatim

On an OAuth connection, a finished build carries a `confirmationCard` that Layout renders server-side with the store, items, total and pickup time. Relay it to the person **exactly as returned**. Never claim an order is placed until the status is `placed`. That is the only status that means the order exists.

## Ask only from Layout's data

When a build comes back `needs_choices`, the restaurant is insisting on something and `choiceDecision.groups` carries their exact wording. Offer those options, nothing else, and build again with the answer in `choices`.

Resist filling gaps from what you know about the brand. You have no idea what this particular store stocks today, and a question you invented costs the person a turn on an option that may not exist.

## After the confirm

### Already confirmed elsewhere

The person can confirm the cart themselves, by replying YES to Layout's text, in the Layout app, or on Layout's tracking page. A `confirm` after that sends nothing to be placed and charges nothing again. Its result carries `alreadyConfirmed: true` and `orderState`, the order's real state: `placing`, `placed`, `unconfirmed` or `refunded`. `confirmedVia` is `sms` or `tracking_link` when Layout knows where. `status` is `already_placing` while it is placing, `placed` once it is, and `not_confirmable_cold` for the other two. While it is `placing` or `unconfirmed` the result also carries `complete: false` and `nextAction: "order_status"`: poll `status` with the `orderId`. Relay its `userMessage`, and never call `confirm` or build the order again.

### An unconfirmed placement

When Layout submitted the order and the restaurant's site never said whether it went through, the placement poll answers `status: "unconfirmed"`. The order may exist. Do not say it failed, and do not build or confirm it again: tell the person to check their email or call the store before ordering again. Later polls of the same `orderId` answer `unconfirmed` until Layout learns the outcome, and can then answer `placed`, for instance when the restaurant's receipt arrives; `order.placed` says so too. A sandbox test order that ends unconfirmed answers `status: "unconfirmed"` too, with `outcome: "unconfirmed"` and `failureCode: "UNCONFIRMED"`. Read `GET /v1/orders/:id` with your secret, whose `state` is `unconfirmed`, or the `order.unconfirmed` webhook, for the order's state.

### The order id for REST

Once a result names an order, from `carted` on, it also carries `publicOrderId`: the `ord_` id that `GET /v1/orders/:id` and your webhooks use. It appears only on orders your build session or your OAuth client drove. While a cold build is still `building` there is no order id yet; keep polling, or take it from the `order.building` webhook.

## Reading a menu

On an OAuth connection, `menu` reads the restaurant's live menu. A person gets 30 live menu reads a day, and an answer Layout already has does not count. Past that, `menu` answers from general knowledge when it can (`status: "knowledge"`, `live: false`), and otherwise comes back `limit_reached`; with `retryAfterSec`, wait that long. Never quote a knowledge answer as the store's current prices. A production build session has no `menu` tool. A sandbox build session's `menu` answers only with the menu Layout holds, and with `code: "menu_not_held"` when it holds none.

## Texts Layout sends

Every text Layout can send a person about an order your application started, over REST or MCP, for a user you provision or through an OAuth client you created. Each comes from Layout's number. In sandbox nothing is texted.

Every other text, including verification codes and connect links, with its exact copy, is on [Texts Layout sends](https://developer.layout.link/reference/texts).

| Text | When Layout sends it | Can you turn it off |
| --- | --- | --- |
| The cart, to reply YES | A ready cart that nothing confirmed or canceled about 15 seconds after it landed (see [If the card is missed](#if-the-person-misses-the-card)). Layout answers the person's YES in the same conversation. | Yes, with **Cart-ready text** |
| Cart reminder | Once, a few minutes later, when that cart is still waiting. | Yes, with **Cart-ready text** |
| Order code | A production confirm that needs the person's code. It names the total, the store and the card (see [Confirm with the person's code](#confirm-with-a-relayed-code), or [code_required](https://developer.layout.link/reference/carts#code-required) over REST). | No |
| Approval link | The restaurant's checkout asks the person to approve the purchase, and `order.challenge` is sent. It names the total and the store. If the person asks for a change on that page and it cannot be made, Layout texts that nothing was ordered or charged. | No |
| Order placed | The order is placed. It says so, with the pickup details Layout has at that moment. | Yes, with **Order texts** |
| Pickup time | The pickup time arrives after the order was placed (see [Pickup time](https://developer.layout.link/reference/api#pickup-time)). One text with the time. | Yes, with **Order texts** |
| Order did not go through | The order failed, or its cart failed while it was building. | No |

Both settings are on the App details page of the console, are on for every application, and cover every order your application starts. A change applies to every text not sent yet, including for orders already in flight.

Turn **Order texts** off when your product tells the person their order is placed and when to pick it up, and you want them to hear it only from you. Your application then owns both: read them from `order.placed`, `order.updated` and [GET /v1/orders/:id](https://developer.layout.link/reference/api#get-v1-orders-id). It never stops Layout's receipt email or push notifications. A person you provisioned who has no email on their Layout profile and no Layout app then hears nothing from Layout about the order, so tell them yourself.

Two orders are always texted, whatever the setting:

- An order Layout already texted a failure for, that is placed after all (the store's email confirms it later). The person has to hear that it went through, or they may order again.
- An order the person confirmed by replying YES to Layout's cart-ready text. They are already in a text thread with Layout, and its reply told them a text would follow.

No setting stops an order code, an approval link or the text saying an order did not go through. A person who never hears that an order failed may order again and be charged twice.

Texts that set a person up, such as a verification code or a connect link, are on [Provisioning](https://developer.layout.link/reference/provisioning).

### If the card is missed

If nothing confirms or cancels a ready cart about 15 seconds after it lands, Layout texts the person the same cart: the store's name and address, each item, the total, and the card it goes on. For a cart your application built, the text opens with your application's name as it is in the console: `Acme Assistant built your order at Layout test kitchen, 1 Sandbox Way, San Francisco, CA 94107:`. They reply YES to place it; once a code is out for the cart, a YES points them to the code instead. If the cart is still waiting a few minutes later, Layout sends one reminder. A cart already confirmed or canceled, even while it was still building, gets no text, and neither does a number that opted out of Layout texts. An order that needs an extra confirmation step tells them to open the Layout app instead of replying.

If your own chat carries the confirm and you want the person to hear only from you, turn off **Cart-ready text** on the App details page of the console. A build session then confirms itself, with the person's code when one is due.
