Carts


Build a real cart at a restaurant, show the person its total, and confirm it, over REST. A cart is built and unpaid. An order is confirmed. Layout never charges without the person's confirm.

Who can call what

Carts act for one person, so they take a credential that names one: a connected person's access token (see Authentication), or a build grant from POST /v1/users/:id/grant. Your application secret does not open a cart.

CallConnected person's tokenBuild grant
Build, read, answer a cartYesYes
Confirm, resend the codeYesYes, with the code Layout texts the person on their first live order through your application, and after that when ordering anywhere else in Layout would ask for one (see Confirming with a build grant). In sandbox a grant's confirm is a simulation and takes no code.
CancelYesYes, a cart your application built.
Read an orderYes, that person's orders your application droveNo. Use your application secret.
Read the food profileYesNo

POST /v1/carts

Starts a real build on the restaurant's own ordering site and answers at once. Nothing is charged.

curl https://api.layout.link/v1/carts \
  -H "Authorization: Bearer $PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "request": "an iced oat latte, large",
    "restaurantName": "Layout test kitchen",
    "near": "Market St, San Francisco",
    "timeZone": "America/Los_Angeles",
    "idempotencyKey": "7f3c2a9e-cart-1"
  }'
202 Accepted

{ "cart": { "id": "crt_9b1f0c2a4eN2YzYzJhOWUtY2FydC0x", "status": "building" } }
FieldRequiredNotes
requestOne of these twoWhat the person wants, in their words, up to 200 characters.
itemsOne of these twoUp to 10 entries, one per item. Sent alone, together they must fit in 200 characters.
placeIdOne way to name the storeThe store, from a places search. The most exact way in.
orderUrlOne way to name the storeThe restaurant's own https ordering page.
restaurantName with near or geoOne way to name the storeLayout resolves the store. When more than one matches, the answer is 409 store_ambiguous with candidates: send the placeId the person picks, with the same key.
timeZoneNoThe person's IANA zone, so "is this pickup today" is answered on their clock.
idempotencyKeyYes, or an Idempotency-Key header8 to 100 characters of letters, digits, ., _, : and -. One per real intent. The same key and body returns the same cart and never builds twice. The same key with a different body is 409 idempotency_conflict. Leave it out and send the Idempotency-Key header instead, and the header’s value is used. A repeat through the header returns the first answer, so it can still say building: read GET /v1/carts/:id for the cart’s state. See the body’s idempotencyKey.

A cart id belongs to your application and to that person. Presented by another application, or for another person, it is a 404.

GET /v1/carts/:id

Poll until the cart is ready, needs an answer, or failed. A cold build takes two to three minutes; a store Layout knows well takes seconds.

200 OK

{
  "cart": {
    "id": "crt_9b1f0c2a4eN2YzYzJhOWUtY2FydC0x",
    "status": "ready",
    "store": { "name": "Layout test kitchen", "address": "1 Market St, San Francisco, CA" },
    "items": [{ "name": "Iced Oat Latte", "quantity": 1, "priceMinor": 650, "modifiers": ["Large"] }],
    "currency": "usd",
    "totalMinor": 750,
    "fees": {
      "subtotalMinor": 650, "taxAndFeesMinor": 60, "merchantTotalMinor": 710,
      "serviceFeeMinor": 40, "serviceFeeWaived": false, "creditMinor": 0
    },
    "card": { "brand": "visa", "last4": "4242" },
    "codeRequired": true,
    "confirmationCard": "Layout test kitchen\n1x Iced Oat Latte (Large)\nTotal $7.50 on your card ending 4242",
    "decision": null,
    "failure": null,
    "order": { "id": "ord_7c21e4b9a0d3", "status": "carted" },
    "expiresAt": "2026-10-01T17:15:00.000Z"
  }
}

totalMinor is the only total. It is the restaurant's total plus Layout's service fee when the fee is not waived, and it is exactly what confirm takes. Show it to the person, and send it back unchanged. Never add up fees yourself. Layout credit, when the person has some, is applied when the order is placed: the card is charged totalMinor minus fees.creditMinor.

The person can change a ready cart in Layout. When they do, the cart reads at its new totalMinor, its items are the changed lines without per-item prices, and confirmationCard is null, because Layout's earlier wording no longer describes it. Show the person what is there now.

Show the person the amount, the items and the card before they confirm. confirmationCard is Layout's own wording of all three, to show as written. card and confirmationCard are present only on a connected person's token.

statusMeaningWhat you do
buildingLayout is on the restaurant's site.Poll again in a few seconds.
readyPriced and confirmable until expiresAt, fifteen minutes from when it was ready.Show it, then confirm or cancel.
needs_answerThe restaurant needs a choice, or the earliest pickup is not today. Nothing was spent.Ask the person decision.question and answer it.
failedThe cart could not be built. failure.code is stable, failure.message is written for the person.Tell them, and build again with a new key if they want.
expiredNobody confirmed it in time. Nothing was charged.Build again with a new key.
canceledReleased.Nothing.
closedIt can no longer be confirmed, usually because it was.Read order, or GET /v1/orders/:id.

POST /v1/carts/:id/answer

When a cart needs an answer, decision says which kind and how to send it.

"decision": {
  "kind": "choices",
  "question": "Which milk for the Iced Oat Latte?",
  "item": "Iced Oat Latte",
  "groups": [{ "name": "Milk", "required": true, "options": ["Oat", "Almond"] }],
  "answerWith": { "field": "choices" }
}
curl https://api.layout.link/v1/carts/crt_9b1f0c2a4eN2YzYzJhOWUtY2FydC0x/answer \
  -H "Authorization: Bearer $PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "choices": "oat", "idempotencyKey": "7f3c2a9e-answer-1" }'

Send exactly one of choices (the person's words, up to 300 characters) or acceptPickupTime: true, which accepts the time a pickup_time decision states. You never type a time. An answer builds the cart again with it, as a new cart: the 202 carries a new id. Poll that one.

POST /v1/carts/:id/confirm

A connected person's token or a build grant. Run it only after the person has seen the cart and said yes. On a live build grant the code is required on the person's first live order through your application (see Confirming with a build grant); in sandbox a grant's order is simulated.

curl https://api.layout.link/v1/carts/crt_9b1f0c2a4eN2YzYzJhOWUtY2FydC0x/confirm \
  -H "Authorization: Bearer $PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "expectedTotalMinor": 750, "idempotencyKey": "7f3c2a9e-confirm-1" }'
202 Accepted

{ "order": { "id": "ord_7c21e4b9a0d3", "status": "placing" } }

Confirm never answers placed. placing means the order was handed off. Read GET /v1/orders/:id, or wait for the order.placed, order.failed or order.unconfirmed webhook. Treat unconfirmed as pending, never as failed: retrying it can charge the person twice.

If the confirm times out, send the same call again with the same key. Never a new key. The retry gets the answer the first call earned, or 409 in_progress while it is still running; the person is charged once either way. See Retrying a confirm.

expectedTotalMinor must be the cart's totalMinor. If the price moved, the answer is 409 price_changed and nothing is confirmed or charged: read the cart again and show the person the new total. The error deliberately carries no figure.

When the answer is code_required

On a connected person's token, a live confirm needs the 6-digit code Layout texts the person exactly when ordering anywhere else in Layout would: the person keeps codes on, the restaurant's total (fees.merchantTotalMinor) is over Layout's amount limit for codes, which can change, Layout's risk checks ask for one, or a code is already out for this cart. Otherwise the first confirm answers 202 placing and no code is involved. On a live build grant the code is also required on the person's first live order through your application. codeRequired on the cart says what to expect; the confirm still decides, so handle code_required on every confirm.

  1. Call confirm. The answer is 400 code_required, and Layout texts the person a code.
  2. Ask the person for it.
  3. Send the same request again, with the same idempotencyKey, plus "code": "123456".

A wrong code is 400 code_invalid: ask them to check it. A code that expired, was already used, was replaced by a newer one, or was texted for a different total is 400 code_expired: text a new one with resend-code. Too many wrong codes is 429 code_attempts_exceeded. When the person's number has replied STOP to Layout, no code can reach them and the answer is 409 texts_blocked: they reply START to the number Layout texts from, then you confirm again. Nothing is confirmed or charged on any of these.

Confirming with a build grant

Until a person has approved an order through your application with the code Layout texts them, every build grant confirm needs that code, whatever the total and whatever their own code setting. That code's text says so: sharing it places the order and lets your application place their future orders without a code. Using it to place the order is how the person, not your application, agrees to that. After that, a build grant's confirm asks for the code exactly when a connected person's token would: the person keeps codes on, the total is over Layout's amount limit for codes, which can change, Layout's risk checks ask for one, or a code is already out for the cart. Otherwise the first confirm answers 202 placing. Only that code counts: an order the person placed by replying to Layout's text, in the Layout app, or through an OAuth connection does not, and neither does a code for another application. The consent ends when the person disconnects your application, you deprovision them, or they connect to it again, and the code is owed again from the next confirm. codeRequired on the cart tells you which case you are in, and the confirm decides.

  1. Show the person the cart's items, store and totalMinor, and wait for their yes.
  2. Call confirm with no code. When no code is due, the answer is 202 placing and you are done. When one is, the answer is 400 code_required, and Layout texts the person from Layout's number: Layout: 482913 approves $7.50 at Layout test kitchen on your Visa ending 4242. Sharing this code places the order and lets Acme Assistant place future orders for you without a code. Turn on order codes in Layout to stop that. It expires in 10 minutes. If you did not just order, ignore this text. The text names the total, the store and the card that will be billed, in Layout's words. Until the person has approved an order through your application with such a code, it also names your application, as your application's name in the console, and says that sharing the code lets it place future orders without one; a later code leaves that sentence out. If a code without that sentence is already out for the cart, the next confirm without a code texts a fresh one with it. Nothing your application sends appears in it.
  3. Ask the person for the code, then send the same request again with the same idempotencyKey, plus "code". The answer is 202 placing.

A second confirm without a code does not text again while the last code is still good (10 minutes); its message asks you for that code, and resend-code texts a fresh one. Once the code has expired, or its text never went out, the next confirm without a code texts a new one. A text that could not be sent is 502 send_failed, not code_required: retry shortly. Layout texts at most four codes per cart, and your application has a daily code allowance; past either the answer is 429 rate_limited and nothing is texted. The code works once, for this cart and this total, for 10 minutes. Every other check still runs: the price lock, the person's daily limit, Layout's risk checks, and whether the restaurant's session is still open. Only carts your application built can be confirmed; any other cart id is a 404.

The person must have finished joining Layout first. Until then the confirm answers 403 build_only, no code is texted, and error.next.step says which step is missing: sign_up_required, they sign up to Layout with the number you provisioned and add a card; connect_required, the number already has a Layout account, so you send them a connect link (POST /v1/users/:id/connect) and they approve it; link_expired, they were provisioned too long ago and you provision them again; phone_required or card_required, they still need to verify their number or add a card. When Layout has a page where the person finishes the step, error.next.url is it, so you can send it to them. See Provisioning.

403 Forbidden

{
  "error": {
    "code": "build_only",
    "message": "This person has not finished joining Layout, so the cart cannot be confirmed yet. They need to sign up to Layout with this number and add a card, then you confirm again. Nothing was charged.",
    "orderId": "ord_7c21e4b9a0d3",
    "next": { "step": "sign_up_required", "url": "https://account.layout.link/join" }
  }
}

Never ask the person for a Layout sign-in code, and never reuse a code from another cart. An order code cannot sign anybody in; it only approves the one cart it names.

POST /v1/carts/:id/confirm/resend-code

Texts the person a fresh code and answers 200 { "codeSent": true }. The new code replaces any code already out for this cart. Use it when the person never got the code, or it expired. A text that could not be sent is 502 send_failed: retry shortly. Several codes in a row is 429 rate_limited with Retry-After. A build grant asking for a fifth code for one cart, or past its application's daily code allowance, also gets 429 rate_limited, without Retry-After. A number that replied STOP is 409 texts_blocked. A cart that needs no code, including a build grant's cart after the person's first order when no rule asks for one, answers 409 code_not_needed and nothing is texted; so does every sandbox cart, because a simulated order takes no code.

POST /v1/carts/:id/cancel

A connected person's token, or a build grant for a cart your application built. Releases this cart and nothing else, and answers 200 { "cart": { "id": "crt_…", "status": "canceled" } }. Call it when the person says no: an unreleased cart holds a live session at the restaurant until it expires. A cart that is still building is 409 cart_not_ready; one already confirmed is 409 cart_closed, and is not canceled.

GET /v1/users/me/profile

A connected person's token only. What Layout applies to every cart it builds for them.

200 OK

{
  "profile": {
    "applyDietaryRestrictions": true,
    "preferences": [{ "category": "dietary", "note": "no dairy" }]
  }
}

No name, contact detail, card or address is ever returned here.

Sandbox

A sandbox build grant, or a connected sandbox test account, confirms only as a simulation. No code is asked for, nothing is ordered or charged, the confirm answers placing with "simulated": true, and the order reads placed with simulated: true. The build itself is real: it runs on the restaurant's real site.

Errors

Every error has the one shape described in Errors and status. These are the codes carts add.

StatusCodeMeaning
400invalid_requestA field is missing or malformed. The message names it.
400code_requiredAsk the person for the code Layout texted and send the same request with it.
400code_invalid / code_expiredThe code did not match, or is no longer valid: expired, used, replaced, or texted for another total.
402card_declinedThe person's card was declined. Nothing was placed.
403build_onlyThe person has not finished joining Layout. The message says which step is missing (sign up and add a card, approve a connect link, be provisioned again, verify their number, add a card). Nothing was texted or charged.
403risk_deniedLayout will not place this order.
404not_foundNo such cart for this application and person.
409store_ambiguousPick one of candidates and send its placeId.
409store_not_foundNo orderable store matched.
409price_changedThe total moved. Read the cart again and show the person the new total.
409over_daily_capOver the person's daily spending limit. It resets at midnight UTC.
409cart_not_ready / cart_closedStill building, or no longer confirmable.
409texts_blockedThe person's number replied STOP to Layout, so no code can be texted. They reply START to the number Layout texts from, then you try again.
409answer_not_needed / nothing_to_cancel / code_not_neededThe cart is not in the state that call needs.
409idempotency_conflict / in_progressThe key was used for something else, or that request is still running. in_progress carries Retry-After.
409over_order_limit / not_placedNot placed: over Layout's per-order limit, or the restaurant's site refused it.
429build_limitA daily build budget is spent. The message names which, and error.limit carries its scope, max and resetsAt.
429code_attempts_exceeded / rate_limitedToo many wrong codes, or too many codes or calls in a row. Wait for Retry-After.
500internalThe outcome is not known. Read the order before you do anything else, and do not build it again until the order says where it is.
502send_failedThe code could not be texted. Nothing was charged. Retry shortly.
503unavailableA check could not run. Nothing was confirmed or charged. Retry the same request.