# 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](https://developer.layout.link/reference/authentication)), or a build grant from `POST /v1/users/:id/grant`. Your application secret does not open a cart.

| Call | Connected person's token | Build grant |
| --- | --- | --- |
| Build, read, answer a cart | Yes | Yes |
| Confirm, resend the code | Yes | Yes, 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](#relayed-code)). In sandbox a grant's confirm is a simulation and takes no code. |
| Cancel | Yes | Yes, a cart your application built. |
| Read an order | Yes, that person's orders your application drove | No. Use your application secret. |
| Read the food profile | Yes | No |

## POST /v1/carts

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

```bash
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" } }
```

| Field | Required | Notes |
| --- | --- | --- |
| `request` | One of these two | What the person wants, in their words, up to 200 characters. |
| `items` | One of these two | Up to 10 entries, one per item. Sent alone, together they must fit in 200 characters. |
| `placeId` | One way to name the store | The store, from a places search. The most exact way in. |
| `orderUrl` | One way to name the store | The restaurant's own https ordering page. |
| `restaurantName` with `near` or `geo` | One way to name the store | Layout 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. |
| `timeZone` | No | The person's IANA zone, so "is this pickup today" is answered on their clock. |
| `idempotencyKey` | Yes, or an `Idempotency-Key` header | 8 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](https://developer.layout.link/reference/limits#body-keys). |

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.

| `status` | Meaning | What you do |
| --- | --- | --- |
| `building` | Layout is on the restaurant's site. | Poll again in a few seconds. |
| `ready` | Priced and confirmable until `expiresAt`, fifteen minutes from when it was ready. | Show it, then confirm or cancel. |
| `needs_answer` | The restaurant needs a choice, or the earliest pickup is not today. Nothing was spent. | Ask the person `decision.question` and answer it. |
| `failed` | The 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. |
| `expired` | Nobody confirmed it in time. Nothing was charged. | Build again with a new key. |
| `canceled` | Released. | Nothing. |
| `closed` | It can no longer be confirmed, usually because it was: by your confirm, or by the person on another surface (see [Already confirmed elsewhere](#confirmed-elsewhere)). | Read `order`: `order.status` is the order's state now, and `order.confirmedVia` says where the person confirmed it when Layout knows. Or read `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" }
}
```

```bash
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](#relayed-code)); in sandbox a grant's order is simulated.

```bash
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](https://developer.layout.link/reference/limits#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.

### Already confirmed elsewhere

The person can confirm your cart without you: by replying YES to Layout's cart-ready text (see [If the person misses the card](https://developer.layout.link/reference/mcp#if-the-person-misses-the-card)), in the Layout app, or on Layout's tracking page. Exactly one confirm places the order. When the cart's order was already confirmed, your confirm answers `409 already_confirmed`, sends nothing to be placed, and charges nothing again. The answer names the order and its real state now; it never says `placed` before the order is.

```
409 Conflict

{
  "error": {
    "code": "already_confirmed",
    "message": "This cart was already confirmed by the person replying YES to Layout's text, so nothing was confirmed or charged again. It is placing now. Read GET /v1/orders/ord_7c21e4b9a0d3, and do not confirm or build this order again.",
    "orderId": "ord_7c21e4b9a0d3",
    "order": { "id": "ord_7c21e4b9a0d3", "status": "placing", "confirmedVia": "sms" },
    "userMessage": "This order was already confirmed and is being placed now. Nothing extra was charged."
  }
}
```

`error.order.status` is `placing`, `placed`, `unconfirmed` or `refunded`, the same value `GET /v1/orders/:id` reports. `unconfirmed` means Layout cannot yet tell whether the restaurant received it: it is not failed, so do not build it again. `error.order.confirmedVia` is `sms` (a YES to Layout's text) or `tracking_link` (Layout's tracking page) when Layout knows, and absent otherwise, including a confirm in the Layout app or another of your own requests. `error.userMessage` is a sentence you can show the person as written. Follow the order with `GET /v1/orders/:id` or the `order.*` webhooks, which go out whichever surface confirmed it.

A retry of your own confirm with the same `idempotencyKey` still gets the answer that call earned: a `202 placing` stays `202 placing`. Only a confirm that did not place the order answers `already_confirmed`. A cart whose order failed or expired answers `409 cart_closed` as before.

### 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](https://developer.layout.link/reference/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 already confirmed is `409 already_confirmed`, with the order, and nothing is texted. 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](https://developer.layout.link/reference/errors). These are the codes carts add.

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | A field is missing or malformed. The message names it. |
| 400 | `code_required` | Ask the person for the code Layout texted and send the same request with it. |
| 400 | `code_invalid` / `code_expired` | The code did not match, or is no longer valid: expired, used, replaced, or texted for another total. |
| 402 | `card_declined` | The person's card was declined. Nothing was placed. |
| 403 | `build_only` | The 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. |
| 403 | `risk_denied` | Layout will not place this order. |
| 404 | `not_found` | No such cart for this application and person. |
| 409 | `store_ambiguous` | Pick one of `candidates` and send its `placeId`. |
| 409 | `store_not_found` | No orderable store matched. |
| 409 | `price_changed` | The total moved. Read the cart again and show the person the new total. |
| 409 | `over_daily_cap` | Over the person's daily spending limit. It resets at midnight UTC. |
| 409 | `cart_not_ready` / `cart_closed` | Still building, or no longer confirmable. |
| 409 | `texts_blocked` | The 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. |
| 409 | `already_confirmed` | The cart was already confirmed, by the person on another surface or by another of your requests. Nothing was confirmed or charged again. `error.order` is the order and its state now; read it, and do not confirm or build it again. |
| 409 | `answer_not_needed` / `nothing_to_cancel` / `code_not_needed` | The cart is not in the state that call needs. |
| 409 | `idempotency_conflict` / `in_progress` | The key was used for something else, or that request is still running. `in_progress` carries `Retry-After`. |
| 409 | `over_order_limit` / `not_placed` | Not placed: over Layout's per-order limit, or the restaurant's site refused it. |
| 429 | `build_limit` | A daily build budget is spent. The message names which, and `error.limit` carries its `scope`, `max` and `resetsAt`. |
| 429 | `code_attempts_exceeded` / `rate_limited` | Too many wrong codes, or too many codes or calls in a row. Wait for `Retry-After`. |
| 500 | `internal` | The 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. |
| 502 | `send_failed` | The code could not be texted. Nothing was charged. Retry shortly. |
| 503 | `unavailable` | A check could not run. Nothing was confirmed or charged. Retry the same request. |
