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
- Provision.
POST /v1/userswith your secret, a name and a phone number. The answer'sidis the person'susr_id, andnextsays how to connect them. - 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). Then
POST /v1/users/:id/verify/start, then/verifywith the code the person reads back. That makes their Layout account. Then send them a card link. Someone who already signs in to Layout approves a connect link instead. Building does not wait for this step; a production confirm does. - Grant.
POST /v1/users/:id/grantwith your secret mints anlgb_build grant for that person. It lasts 15 minutes, and you refresh it before it expires. - Build a cart. With the grant,
POST /v1/cartsover REST, or theordertool'sbuildover MCP. Poll until it is ready (readyon REST,cartedon MCP), needs an answer, or failed. See Carts. - Show it and wait for yes. Show the person the cart's
totalMinor, items and store. Nothing is charged until they say yes. - 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. Over REST the answer is
placing, neverplaced; over MCP a sandbox confirm answersplacedwithsimulated: true. - Follow the outcome. The order ends
placed,unconfirmedorfailed; a refund on a placed order changes nothing instateand arrives asrefunded_minorand anorder.refundedevent. Webhooks tell your backend, andGET /v1/orders/:idreads it.unconfirmedmeans Layout cannot tell whether the restaurant received the order: never treat it as failed, and never build it again. - Disconnect.
DELETE /v1/users/:idwhen they leave your app. See Disconnect a person.
A person who signs in with their own Layout account through 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).
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. | 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. | 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 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 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 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.
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 readscodeReason: "risk_check"and whose code is always000000. Nothing is texted. - Reading it in advance. Over REST, a ready cart's
codeRequiredandcodeReasonare checked each time you read it. Over MCP, a person's own connection readsconfirmation.method: "sms_code"on the carted build. On either door the confirm decides, so handlecode_requiredon every confirm. - Before any code. A person who has not verified, connected or added a card is refused with
build_onlyand nothing is texted. See Confirming with a build grant.
The code steps, errors and limits are on Carts for REST and Ordering over MCP for MCP.