# How Layout works

One order end to end, the credentials, how REST and MCP map, and when a confirm needs a code.

## The order lifecycle

1. **Provision.** `POST /v1/users` with your secret, a name and a phone number. The answer's `id` is the person's `usr_` id, and [next](https://developer.layout.link/reference/provisioning#next-step) says how to connect them.
2. **Verify.** First tell the person, in your own words, that your app uses Layout to place their orders and that Layout will text them a code (see [Introduce Layout first](https://developer.layout.link/reference/provisioning#introduce-layout-first)). Then `POST /v1/users/:id/verify/start`, then `/verify` with the code the person reads back. That makes their Layout account. Then send them a [card link](https://developer.layout.link/reference/card-links). Someone who already signs in to Layout approves a connect link instead. Building does not wait for this step; a production confirm does.
3. **Grant.** `POST /v1/users/:id/grant` with your secret mints an `lgb_` build grant for that person. It lasts 15 minutes, and you refresh it before it expires.
4. **Build a cart.** With the grant, `POST /v1/carts` over REST, or the `order` tool's `build` over MCP. Poll until it is ready (`ready` on REST, `carted` on MCP), needs an answer, or failed. See [Carts](https://developer.layout.link/reference/carts).
5. **Show it and wait for yes.** Show the person the cart's `totalMinor`, items and store. Nothing is charged until they say yes.
6. **Confirm.** Confirm with that total. In production the confirm asks for the 6-digit code Layout texts the person only when [a code is due](#when-a-code-is-due). Over REST the answer is `placing`, never `placed`; over MCP a sandbox confirm answers `placed` with `simulated: true`.
7. **Follow the outcome.** The order ends `placed`, `unconfirmed` or `failed`; a refund on a placed order changes nothing in `state` and arrives as `refunded_minor` and an `order.refunded` event. [Webhooks](https://developer.layout.link/reference/webhooks) tell your backend, and `GET /v1/orders/:id` reads it. `unconfirmed` means Layout cannot tell whether the restaurant received the order: never treat it as failed, and never build it again.
8. **Disconnect.** `DELETE /v1/users/:id` when they leave your app. See [Disconnect a person](https://developer.layout.link/reference/provisioning#disconnect-a-person).

A person who signs in with their own Layout account through [Sign in with Layout](https://developer.layout.link/reference/sign-in-with-layout) skips steps 1 to 3: their OAuth access token builds and confirms for them. If nobody confirms or cancels a ready cart within a few seconds, Layout texts the person the cart and they can reply YES to place it, unless you turned that text off for your application. Your confirm then answers `already_confirmed` (see [Already confirmed elsewhere](https://developer.layout.link/reference/carts#confirmed-elsewhere)).

## The credentials

Every request carries exactly one bearer token. Which one decides who the call acts for and what it can do.

| Credential | Acts for | Where it works | Lifetime | In examples |
| --- | --- | --- | --- | --- |
| Application secret, `sk_test_…` / `sk_live_…` | Your application | `/v1/users` and its verify, connect, grant and links routes, `DELETE /v1/users/:id`, `GET /v1/orders/:id`, `/v1/events`, `/v1/usage`, `/v1/sandbox/webhooks/fire`, `/v1/whoami`. Never a cart, place, menu or image route, and never MCP. | Until you rotate it | `$LAYOUT_SECRET_KEY` |
| Build grant, `lgb_…` | One person you provisioned | The REST person routes except the food profile, and MCP with three tools. See [What each door can do](#what-each-door-can-do). | 15 minutes, refreshable up to 24 hours | `$LAYOUT_GRANT` |
| OAuth access token | One person who approved your application on Layout's consent screen | Every REST person route, or every MCP tool. A token is for one resource: `https://api.layout.link` or `https://mcp.layout.link`. | 10 minutes, with a 30-day rotating refresh token | `$LAYOUT_PERSON_TOKEN` |
| CLI token, `lcli_…` | Your developer account | `/v1/cli` only. See [Layout CLI](https://developer.layout.link/reference/cli). | 30 days, or what you chose for a CI token | `$LAYOUT_CLI_TOKEN` |

Every example on these pages reads a credential from the variable in the last column. The [Layout CLI](https://developer.layout.link/reference/cli) reads `LAYOUT_SECRET_KEY` and `LAYOUT_CLI_TOKEN` under the same names. The webhook signing secret is not a bearer token: it verifies what Layout sends you. See [Authentication](https://developer.layout.link/reference/authentication) for each credential in full.

## Two doors, one order

REST at `https://api.layout.link/v1` and MCP at `https://mcp.layout.link` drive the same engine, with the same checks and the same money rules. The names differ. This is the whole mapping.

### What each door can do

| Step | REST | MCP | Build grant |
| --- | --- | --- | --- |
| Find a store | `GET /v1/places` | `places` | Both doors |
| One store's facts | `GET /v1/places/:placeId` | `place_facts` | REST only |
| Read a menu | `POST /v1/menus`, `GET /v1/menus/:menuId` | `menu` | REST only |
| An item's photo | `POST /v1/images/items`, `GET /v1/images/items/:imageId` | `item_photo` | REST only |
| Resolve the store first | None: `POST /v1/carts` resolves it, or answers `409 store_ambiguous` | `order`, `action: "preview"` | Yes, on MCP |
| Build a cart | `POST /v1/carts` | `order`, `action: "build"` | Both doors |
| Follow the build | `GET /v1/carts/:cartId` | `order`, `action: "status"` with the `idempotencyKey`, or `order_status` | Both doors |
| Answer a choice or a pickup time | `POST /v1/carts/:cartId/answer` | `order`, `action: "build"` again with a new `idempotencyKey`, and the answer in `choices`, or `pickupDecision.stated` as `acceptPickupTime` | Both doors |
| Confirm | `POST /v1/carts/:cartId/confirm` | `order`, `action: "confirm"` with the `orderId` | Both doors |
| Text a fresh code | `POST /v1/carts/:cartId/confirm/resend-code` | `order`, `action: "confirm"` with `stage: "resend"` | Both doors |
| Release a cart | `POST /v1/carts/:cartId/cancel` | `order` with `action: "status"`, `cancel: true` and the `orderId`, or while it builds the build's `idempotencyKey` (`cancel_build`) | Both doors, for a cart your application built |
| Follow the order | `GET /v1/orders/:orderId` | `order`, `action: "status"` with the `orderId`, or `order_status` | Both doors |
| The person's food profile | `GET /v1/users/me/profile` | `profile` | Neither door |
| Recommendations, a location link, Layout's help centre | None | `discover`, `get_location`, `help` | Neither door |

A person's OAuth access token can make every call in its resource's column. A build grant session over MCP lists only `order`, `order_status` and `places`, and in `order` it cancels only a cart its application built, named by its `orderId` or, while it builds, by the build's `idempotencyKey`. Over REST the grant also reads place facts, menus and item photos. [mcp-tools.json](https://developer.layout.link/mcp-tools.json) lists each session's tools and actions, read off the server.

### Names and states

| What | REST | MCP |
| --- | --- | --- |
| Still building | `cart.status: "building"` | `build.status: "building"` |
| Priced and confirmable | `cart.status: "ready"` | `build.status: "carted"` |
| The restaurant needs a choice | `cart.status: "needs_answer"`, `decision.kind: "choices"`, options in `decision.groups` | `build.status: "needs_choices"`, options in `choiceDecision.groups` |
| The earliest pickup is not today | `cart.status: "needs_answer"`, `decision.kind: "pickup_time"` | `build.status: "pickup_next_day"`, with `pickupDecision` |
| Could not be built | `cart.status: "failed"`, with `failure.code` | Any other `build.status`: relay `userMessage` |
| Nobody confirmed in time | `cart.status: "expired"` | `build.status: "cart_expired"` |
| Released | `cart.status: "canceled"`, or `"canceling"` then `"canceled"` when it was still building | `cancel.status: "canceling"`, then `"canceled"` |
| The total to confirm with | `cart.totalMinor`, sent as `expectedTotalMinor` | The carted build's `totalMinor` (on a build session it includes Layout's service fee), sent as `expectedTotalMinor` |
| A code is needed | `400` with `error.code: "code_required"` and `error.codeReason`; `cart.codeRequired` says so in advance | `confirm.status: "error"` with `confirm.error: "code_required"`, and on a build grant `codeSent` |
| Confirm accepted | `202`, `order.status: "placing"` | `confirm.status: "placing"` (or `"confirmed"`), or `"placed"` with `simulated: true` for a simulated order |
| Placement outcome | The order's `state`: `placed`, `unconfirmed` or `failed` | `status` with the `orderId`: `placed`, `unconfirmed` or `failed` |

Cart fields are camelCase. Order fields, from `GET /v1/orders/:id`, and every webhook field are snake_case, and they do not map one to one: an order's `total_minor` is the restaurant's total without Layout's service fee, never the total to confirm with. See [GET /v1/carts/:id](https://developer.layout.link/reference/carts#get-v1-carts-id).

### Ids

| Id | Shape | Where |
| --- | --- | --- |
| Person | `usr_` and 12 hex characters for a person you provisioned, 24 for one who signed in through OAuth | REST, webhooks, `whoami` |
| Cart | `crt_…`, opaque | REST only. MCP has no cart id: it follows a build by its `idempotencyKey`. |
| Order, public | `ord_…` | REST, webhooks, the events feed, and an MCP result's `publicOrderId` from `carted` on |
| Order, MCP | A UUID, such as `0b6f2a54-…` | MCP results' `orderId`, and what every MCP call that names an order takes |
| Store | `placeId` | Both doors |

## When a code is due

Any one of these makes a production confirm ask for the 6-digit code Layout texts the person, which they give you. When none holds, the confirm goes ahead without a code. A cart's `codeReason` names the first that holds, in table order, or `unverified` when Layout could not check; on the confirm's `code_required`, `risk_check` comes first.

| `codeReason` | When | Build grant | Person's OAuth token |
| --- | --- | --- | --- |
| `first_order_consent` | The person has not yet agreed to your application ordering for them. See Agreement below. | Code | Never asked for this reason |
| `over_limit` | The restaurant's total (`fees.merchantTotalMinor`) is over Layout's limit for orders without a code, which can change. | Code | Code |
| `code_outstanding` | Layout already texted a code for this cart. | Code | Code |
| `risk_check` | Layout's risk checks ask for one on this order. Known only at confirm, so a live cart never says it. | Code | Code |
| `unverified` | Layout could not check just now, so it asks. | Code | Code |
| `null` | None of the above. | No code | No code |

- **Agreement.** Reading back a verify code from a text that names your application, or approving the connect screen, is the person's agreement that your application places orders for them. Without either, the first order's code is the agreement, and its text says so. It lasts until the person disconnects your application or you call `DELETE /v1/users/:id`. A new connection after that needs a new agreement.
- **Sandbox.** No confirm asks for a code, except at the test restaurant `test_place_code`, whose cart reads `codeReason: "risk_check"` and whose code is always `000000`. Nothing is texted.
- **Reading it in advance.** Over REST, a ready cart's `codeRequired` and `codeReason` are checked each time you read it. Over MCP, a person's own connection reads `confirmation.method: "sms_code"` on the carted build. On either door the confirm decides, so handle `code_required` on every confirm.
- **Before any code.** A person who has not verified, connected or added a card is refused with `build_only` and nothing is texted. See [Confirming with a build grant](https://developer.layout.link/reference/carts#relayed-code).

The code steps, errors and limits are on [Carts](https://developer.layout.link/reference/carts#code-required) for REST and [Ordering over MCP](https://developer.layout.link/reference/mcp#confirm-with-a-relayed-code) for MCP.
