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

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.

CredentialActs forWhere it worksLifetimeIn 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 provisionedThe 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 tokenOne person who approved your application on Layout's consent screenEvery 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

StepRESTMCPBuild grant
Find a storeGET /v1/placesplacesBoth doors
One store's factsGET /v1/places/:placeIdplace_factsREST only
Read a menuPOST /v1/menus, GET /v1/menus/:menuIdmenuREST only
An item's photoPOST /v1/images/items, GET /v1/images/items/:imageIditem_photoREST only
Resolve the store firstNone: POST /v1/carts resolves it, or answers 409 store_ambiguousorder, action: "preview"Yes, on MCP
Build a cartPOST /v1/cartsorder, action: "build"Both doors
Follow the buildGET /v1/carts/:cartIdorder, action: "status" with the idempotencyKey, or order_statusBoth doors
Answer a choice or a pickup timePOST /v1/carts/:cartId/answerorder, action: "build" again with a new idempotencyKey, and the answer in choices, or pickupDecision.stated as acceptPickupTimeBoth doors
ConfirmPOST /v1/carts/:cartId/confirmorder, action: "confirm" with the orderIdBoth doors
Text a fresh codePOST /v1/carts/:cartId/confirm/resend-codeorder, action: "confirm" with stage: "resend"Both doors
Release a cartPOST /v1/carts/:cartId/cancelorder 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 orderGET /v1/orders/:orderIdorder, action: "status" with the orderId, or order_statusBoth doors
The person's food profileGET /v1/users/me/profileprofileNeither door
Recommendations, a location link, Layout's help centreNonediscover, get_location, helpNeither 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

WhatRESTMCP
Still buildingcart.status: "building"build.status: "building"
Priced and confirmablecart.status: "ready"build.status: "carted"
The restaurant needs a choicecart.status: "needs_answer", decision.kind: "choices", options in decision.groupsbuild.status: "needs_choices", options in choiceDecision.groups
The earliest pickup is not todaycart.status: "needs_answer", decision.kind: "pickup_time"build.status: "pickup_next_day", with pickupDecision
Could not be builtcart.status: "failed", with failure.codeAny other build.status: relay userMessage
Nobody confirmed in timecart.status: "expired"build.status: "cart_expired"
Releasedcart.status: "canceled", or "canceling" then "canceled" when it was still buildingcancel.status: "canceling", then "canceled"
The total to confirm withcart.totalMinor, sent as expectedTotalMinorThe carted build's totalMinor (on a build session it includes Layout's service fee), sent as expectedTotalMinor
A code is needed400 with error.code: "code_required" and error.codeReason; cart.codeRequired says so in advanceconfirm.status: "error" with confirm.error: "code_required", and on a build grant codeSent
Confirm accepted202, order.status: "placing"confirm.status: "placing" (or "confirmed"), or "placed" with simulated: true for a simulated order
Placement outcomeThe order's state: placed, unconfirmed or failedstatus 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

IdShapeWhere
Personusr_ and 12 hex characters for a person you provisioned, 24 for one who signed in through OAuthREST, webhooks, whoami
Cartcrt_…, opaqueREST only. MCP has no cart id: it follows a build by its idempotencyKey.
Order, publicord_…REST, webhooks, the events feed, and an MCP result's publicOrderId from carted on
Order, MCPA UUID, such as 0b6f2a54-…MCP results' orderId, and what every MCP call that names an order takes
StoreplaceIdBoth 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.

codeReasonWhenBuild grantPerson's OAuth token
first_order_consentThe person has not yet agreed to your application ordering for them. See Agreement below.CodeNever asked for this reason
over_limitThe restaurant's total (fees.merchantTotalMinor) is over Layout's limit for orders without a code, which can change.CodeCode
code_outstandingLayout already texted a code for this cart.CodeCode
risk_checkLayout's risk checks ask for one on this order. Known only at confirm, so a live cart never says it.CodeCode
unverifiedLayout could not check just now, so it asks.CodeCode
nullNone of the above.No codeNo 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.

The code steps, errors and limits are on Carts for REST and Ordering over MCP for MCP.