Quickstart
Your first sandbox order over REST, end to end, in about five minutes. No order you confirm here reaches a restaurant or charges anyone. New to Layout? How Layout works shows the whole lifecycle on one page.
1. Get your credentials
Create an application in the console and copy its sandbox secret from the Credentials page. Keep the secret server-side. Never ship it in a browser or an app binary.
A new application gets sandbox keys straight away. Production is reviewed by a person, and your sandbox keys keep working whatever that review decides. See Environments and approval.
export LAYOUT_SECRET_KEY="sk_test_…"
The Layout CLI reads the same variable. layout test-order runs the sandbox steps below in one command, so you can see it work before you write any code.
2. Provision a user
A name and a phone number. That is the whole signup you have to run.
curl https://api.layout.link/v1/users \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "firstName": "Dana", "lastName": "Whitfield", "phone": "+15005550142" }'
# -> 201 { "id": "usr_4b8e9a01c2d3", "status": "provisioned", "next": "verify", … }
In sandbox the phone must be a reserved test number, +1 500 555 followed by four digits. The answer's id is the person's usr_ id, used below. next says how to connect them: see The next step.
3. Verify the number
In production the person proves the number before you can confirm for them: Layout texts a code, they read it back to you, you send it. In sandbox nothing is texted and the code is always 000000. Optional in sandbox.
curl https://api.layout.link/v1/users/usr_4b8e9a01c2d3/verify/start \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{}'
curl https://api.layout.link/v1/users/usr_4b8e9a01c2d3/verify \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "000000" }'
# -> 200 { "id": "usr_4b8e9a01c2d3", "verified": true, "authorized": true, … }
In production a good code makes the person's Layout account, and the answer reads "status": "active". A person who signs in to Layout on this number themselves connects through a link they approve instead. See Someone who signs in to Layout. Before the first code reaches them, tell them about Layout in your own words: see Introduce Layout first.
4. Add a card
In production, mint a card link and send the person to it. Sandbox needs no card.
curl https://api.layout.link/v1/users/usr_4b8e9a01c2d3/links \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "purpose": "card" }'
# -> 201 { "url": "https://layout.link/h/…", "expiresAt": "…", "purpose": "card" }
5. Mint a build grant
Carts act for one person, so they take that person's credential, never your secret. A build grant is one.
curl -X POST https://api.layout.link/v1/users/usr_4b8e9a01c2d3/grant \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY"
# -> 201 { "token": "lgb_…", "expiresAt": "…", "scope": "build" }
export LAYOUT_GRANT="lgb_…"
A grant lasts 15 minutes. To keep going, send the current token back as { "refresh": "lgb_..." } before it expires. See POST /v1/users/:id/grant.
6. Build a cart
Send what the person asked for in their own words, and the store. Use a fresh random idempotencyKey for every new cart.
curl https://api.layout.link/v1/carts \
-H "Authorization: Bearer $LAYOUT_GRANT" \
-H "Content-Type: application/json" \
-d '{ "request": "a large latte with oat milk", "placeId": "test_place_carted",
"idempotencyKey": "7f3c2a9e-cart-1" }'
# -> 202 { "cart": { "id": "crt_0d928d04f1iQbL1AV4iXSKw2wabMk_B-", "status": "building" } }
export CART_ID="crt_0d928d04f1iQbL1AV4iXSKw2wabMk_B-"
test_place_carted is a sandbox test restaurant with a fixed menu: it builds a ready cart at once, so this total is the same every time. A real store builds the same way in sandbox, from the menu Layout holds, with no browser. Its placeId comes from GET /v1/places, searched with the person's device coordinates so the nearest store comes first. See Finding the nearest store.
Read the cart every 2 to 3 seconds until it is no longer building:
curl https://api.layout.link/v1/carts/$CART_ID \
-H "Authorization: Bearer $LAYOUT_GRANT"
# -> 200 { "cart": { "status": "ready", "totalMinor": 816, "items": […], "store": {…}, "codeRequired": false, … } }
ready means priced and confirmable for 15 minutes. needs_answer means the restaurant needs a choice, and failed means it could not be built. See GET /v1/carts/:id.
7. Confirm
Show the person the cart's totalMinor, items and store, and confirm only after they say yes. Send totalMinor back unchanged as expectedTotalMinor: for this request the sandbox returns 816 (a $7.00 large oat-milk latte, $0.61 tax and Layout's $0.55 service fee); use the number your cart returned.
curl https://api.layout.link/v1/carts/$CART_ID/confirm \
-H "Authorization: Bearer $LAYOUT_GRANT" \
-H "Content-Type: application/json" \
-d '{ "expectedTotalMinor": 816, "idempotencyKey": "7f3c2a9e-confirm-1" }'
# -> 202 { "order": { "id": "ord_7c21e4b9a0d3", "status": "placing", "simulated": true } }
In sandbox the confirm is a simulation: no restaurant receives it, no money moves, and nobody is texted. In production the same call places a real order. The person needs a verified number and a card from a card link. If a code is due, the answer is 400 code_required: send the same request again with their code. On a timeout, retry with the same idempotencyKey.
8. Read the order
curl https://api.layout.link/v1/orders/ord_7c21e4b9a0d3 \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY"
# -> 200 { "id": "ord_7c21e4b9a0d3", "state": "placed", "simulated": true, … }
state ends placed, failed or unconfirmed. unconfirmed means Layout cannot tell whether the restaurant received the order: never treat it as failed, and never build it again. Order fields are snake_case, and the order's total_minor leaves out Layout's service fee. Pickup details can arrive later, each with order.updated. See GET /v1/orders/:id.
9. Get webhooks
Rather than polling, add a subscription on the console's Webhooks page with an HTTPS URL. Layout then POSTs a signed event as the order moves: order.carted when the cart is ready, and order.placed, order.failed or order.unconfirmed at the end. Fire one at your endpoint from sandbox to test it:
curl https://api.layout.link/v1/sandbox/webhooks/fire \
-H "Authorization: Bearer $LAYOUT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "event": "order.placed" }'
Verify every delivery's signature with your sandbox signing secret. See Webhooks and Event types.
Over MCP instead
MCP is the other door to the same ordering, with the same checks. Steps 1 to 5 do not change. Then connect the same lgb_ token to https://mcp.layout.link as a bearer token, with no OAuth step, and call the order tool: preview to resolve the store, build, status until carted, then confirm. A build grant's MCP session has three tools, order, order_status and places, and in sandbox also menu. See Ordering over MCP, and Two doors, one order for how each REST call and state maps to MCP.
DELETE /v1/users/:id disconnects a person. See Disconnect a person.