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

> A cart id can hand you on to another. When a cart reads `status: "closed"` with `replacedBy`, its question was answered and a new cart carries on: read `replacedBy` from then on. This is the one thing a polling loop has to follow.

```js
async function waitForCart(id, token) {
  for (;;) {
    const res = await fetch("https://api.layout.link/v1/carts/" + id, {
      headers: { Authorization: "Bearer " + token },
    });
    const { cart } = await res.json();
    if (cart.status === "closed" && cart.replacedBy) { id = cart.replacedBy; continue; }
    if (cart.status !== "building") return cart;
    await new Promise((r) => setTimeout(r, 2500));
  }
}
```

## 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://docs.layout.link/authentication)), or a build grant from `POST /v1/users/:id/grant`. Your application secret does not open a cart. For the same steps over MCP, see [Two doors, one order](https://docs.layout.link/how-it-works#two-doors).

| Call | Connected person's token | Build grant |
| --- | --- | --- |
| Build, read, answer a cart | Yes | Yes |
| Confirm, resend the code | Yes | Yes, with the person's code when [a code is due](https://docs.layout.link/how-it-works#when-a-code-is-due). In sandbox it is a simulation. |
| Cancel | Yes | Yes, a cart your application built. |
| Read an order | Yes, that person's orders your application drove | Yes, that person's orders your application's build grants built |
| Read the food profile | Yes | No |

## POST /v1/carts

Starts a build on the restaurant's own ordering site and answers at once. Nothing is charged. In sandbox the cart is built from the restaurant's menu as Layout holds it, with no browser: see [Sandbox](#sandbox).

```bash
curl https://api.layout.link/v1/carts \
  -H "Authorization: Bearer $LAYOUT_PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "request": "an iced oat latte, large",
    "placeId": "ChIJbloomCoffee",
    "timeZone": "America/Los_Angeles",
    "idempotencyKey": "7f3c2a9e-cart-1"
  }'
```

```
202 Accepted

{ "cart": { "id": "crt_0d928d04f1iQbL1AV4iXSKw2wabMk_B-", "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 without `request`, together they must fit in 200 characters. |
| `placeId` | One way to name the store | The store, from a places search, and the best way to name one (see [Finding the nearest store](#nearest-store)). If you also send `restaurantName` and it names a different store, the answer is `409 store_mismatch` with `error.store`, and nothing is built. |
| `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. `geo` is the person's coordinates, `{ "lat": 37.7793, "lng": -122.4193 }`. It never picks a store for you: the answer is `409 store_ambiguous` with the nearest open stores as `candidates`. Send the `placeId` the person picks, with the same key. You may keep `restaurantName`. `near` with a street number and name is read as the store's own address, and Layout builds at the branch there. A city or ZIP code is answered with `candidates`. |
| `timeZone` | No | The person's IANA zone, so "today" and times like "6:15 PM" are read on their clock. |
| `fulfillment` | No | `"pickup"`, the default. See [Pickup only](#pickup-only). |
| `simulate` | No, sandbox only | The ending: `placed` (the default), `failed`, `unconfirmed`, `challenge`, `code`, `card_declined`, `price_changed`, `closed` or `pickup_later`. See [Choose the ending](https://docs.layout.link/sandbox#choose-the-ending). |
| `latency` | No, sandbox only | `realistic` (the default) waits about as long as a real build; `instant` skips every wait. See [Timing](https://docs.layout.link/sandbox#timing). |
| `engine` | No, sandbox only | `shadow` (the default) builds from the menu Layout holds. `real` drives the restaurant's own site, 10 a day per developer account. See [The real engine](https://docs.layout.link/sandbox#the-real-engine). |
| `idempotencyKey` | Yes, or an `Idempotency-Key` header | One per real intent, 8 to 100 of `A-Z a-z 0-9 . _ : -`. The same key and body return the same cart and never build twice. The same key with a different body is `409 idempotency_conflict`. You can send the `Idempotency-Key` header instead, by the same rule. See [Idempotency](https://docs.layout.link/limits#idempotency). |

Send only the fields in this table. An unknown field is `400 invalid_request`, here and on answer and confirm, and so is `simulate`, `latency` or `engine` in production. With both `request` and `items`, Layout builds `items` and keeps `request` as the person's words.

A cart id is opaque. It belongs to your application and that person: presented by another application, or for another person, it is a `404`.

### Finding the nearest store

Layout offers the branch nearest the location you send. Search first with [GET /v1/places](https://docs.layout.link/api#get-v1-places), using the best location you have:

- **The device's coordinates**, as `lat` and `lng`, without `near`. `distanceMi` is measured from the person.
- **Their address or ZIP code, geocoded** by your platform into `lat` and `lng`, when you have no location permission.
- **A city or town name**, as `near`, only when you have nothing better. Distances are then mostly measured from its centre. A ZIP code, neighbourhood or landmark sent as `near` comes back with `localityMatched: false`: send coordinates instead.

Build with the `placeId` the person picks, and keep it for their next order there. Send a fresh location when they order somewhere new. Never send the person's own street address as `near`: a cart reads it as the store's address.

## GET /v1/carts/:id

Poll until the cart is ready, needs an answer, or failed. See [How long a build takes](#build-time).

```
200 OK

{
  "cart": {
    "id": "crt_0d928d04f1iQbL1AV4iXSKw2wabMk_B-",
    "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": 763,
    "fees": {
      "subtotalMinor": 650, "taxAndFeesMinor": 60, "merchantTotalMinor": 710,
      "serviceFeeMinor": 53, "serviceFeeWaived": false, "creditMinor": 0,
      "courierFeeMinor": 0, "tipMinor": 0
    },
    "fulfillment": { "type": "pickup", "deliveryUnavailable": null, "dropoff": null, "courierFeeMinor": null, "tip": null },
    "card": { "brand": "visa", "last4": "4242" },
    "codeRequired": false,
    "codeReason": null,
    "confirmationCard": "Layout test kitchen\n1x Iced Oat Latte (Large)\nTotal $7.63 on your card ending 4242",
    "decision": null,
    "failure": null,
    "order": { "id": "ord_7c21e4b9a0d3", "status": "carted" },
    "expiresAt": "2026-10-01T17:15:00.000Z"
  }
}
```

While `status` is `building`, the cart also carries `build`: `{ "stage": "menu", "etaSec": 40 }`. `stage` is `store` (finding the store), `menu` (reading the menu), `item` (building the item) or `cart` (heading to the cart). `etaSec` is a prediction from past builds and can grow on a slow build. In sandbox it counts down the cart's own wait (see [Timing](https://docs.layout.link/sandbox#timing)). It is `null`, or absent, once the cart is not building or when Layout cannot tell yet. Show it as progress and keep polling the cart for the result.

**`totalMinor` is the only total.** It is the restaurant's total plus Layout's service fee, unless the fee is 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, comes off when the order is placed: the card is charged `totalMinor` minus `fees.creditMinor`.

**Cart fields are camelCase; order and webhook fields are snake_case.** An order's `total_minor` is the restaurant's total only, the cart's `fees.merchantTotalMinor`. Never show it as the price or confirm with it. What the card was charged is the order's `amounts.charged_minor` once it is placed.

Before they confirm, show the person the amount, the items and the card. `card` is the card that will be billed, on a person's token and on a build grant, and `null` when the person has no card yet, and on every sandbox cart. On a connected person's token, `confirmationCard` is Layout's own wording of all three, to show as written. On a build grant it is `null`: show the items, the store, `totalMinor` and the card yourself.

`store` is filled whenever Layout knows it, and is `null` only while building or when nothing names the store. Show the address with the name so the person can tell which branch it is.

Each item's `priceMinor` is the price of one of that item, with its options. Multiply by `quantity` for the line. It is `null` when Layout could not read it.

If the person changes a ready cart in Layout, the cart reads at its new `totalMinor`, its `items` lose their per-item prices, and `confirmationCard` is `null`. Show what is there now.

`fulfillment` is `null` until the cart is priced. A pickup reads as in the example, with `fees.courierFeeMinor` and `fees.tipMinor` at `0`.

| `status` | Meaning | What you do |
| --- | --- | --- |
| `building` | Layout is building the cart: on the restaurant's site in production, and from the menu Layout holds in sandbox. | Poll again in 2 to 3 seconds. |
| `ready` | Priced and confirmable until `expiresAt`, 15 minutes after 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](#answer). |
| `failed` | The cart could not be built. `failure.code` is stable, and `failure.message` is written for the person. | Tell them. Build again with a new key if they want. |
| `expired` | Nobody confirmed it before `expiresAt`. `failure.code` is `CART_EXPIRED`, its order reads `failed` with `failure_code` `CART_EXPIRED`, and `order.failed` is sent within a few minutes. Nothing was charged. | Build again with a new key. You need not cancel it. |
| `canceling` | You canceled it while it was building (see [cancel](#cancel)). | Poll until it is `canceled`, or `failed` if the build failed. Never confirm it. |
| `canceled` | Released. Nothing was charged. | Nothing. |
| `closed` | No longer confirmable, usually because it was confirmed: by you, or by the person elsewhere (see [Already confirmed elsewhere](#confirmed-elsewhere)). An answered question cart also reads `closed`, with `replacedBy`, the cart the answer started. | Read `order.status` and `order.confirmedVia`, or `GET /v1/orders/:id`. |

These `failure.code` values come from this API. Any other value is the build's own code, such as `STORE_CLOSED`: show the person `failure.message`.

| `failure.code` | Meaning | What you do |
| --- | --- | --- |
| `build_stopped` | The build stopped before a cart was ready. | Send the same request again, with the same key. |
| `order_in_progress` | An order for this person at this store is confirmed and paying, or another cart is still building for them. `failure.orderId` names the paying order when it is yours. A test order never blocks a real store. | Wait for that order or build to end, then build with a new key. |
| `not_saved` | The cart was priced but could not be saved, so it cannot be confirmed. | Build again with a new key. |
| `retry_blocked` | Several cancels in a row, or many builds in an hour, paused new builds for this person. `failure.retryInSeconds` is the wait. Test restaurants never count. | Show `failure.message`, and build after the wait. |
| `no_card` / `card_not_orderable` | The build needs a card the person does not have, such as one past the day's no-card builds, or Layout cannot use their card. `failure.next` is `{ "step": "card_required" }`. | For a person you provisioned, mint a [card link](https://docs.layout.link/card-links) with `POST /v1/users/:id/links`, send them to it, then build with a new key. On a person's own Layout token, send them to `failure.next.url` as given. |
| `error` | Layout has no reason to give. | Show `failure.message`. |

### How long a build takes

| Store | Typical time to `ready` |
| --- | --- |
| Any store, in sandbox | About 30 seconds for one item and 10 more for each item after it, at most 90, or at once with `"latency": "instant"`. With `"engine": "real"`, as long as in production. A test restaurant is at once, and `test_place_slow` about 20 seconds (see [Timing](https://docs.layout.link/sandbox#timing)). |
| A store Layout knows, in production | Seconds. |
| A store Layout has not ordered from before, in production | Often 60 seconds to 3 minutes. Layout gives a one-item build up to 5 minutes, and a larger order more. |

- **Poll every 2 to 3 seconds.** Faster gains nothing, and every read counts toward your per-minute limit.
- **Stop holding the person after about 4 minutes.** Tell them it is still working, and read the cart every 15 seconds or so. Your timeout does not stop the build, and neither does a [cancel](#cancel).
- **A build always ends.** One that runs out of time reads `failed`.

## 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_0d928d04f1iQbL1AV4iXSKw2wabMk_B-/answer \
  -H "Authorization: Bearer $LAYOUT_PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "choices": "oat", "idempotencyKey": "7f3c2a9e-answer-1" }'
```

When the earliest pickup is not today, the decision is a `pickup_time`:

```
"decision": {
  "kind": "pickup_time",
  "question": "The earliest pickup here is tomorrow at 7:00 AM. Order for then?",
  "pickupTime": "Tomorrow 7:00 AM",
  "answerWith": { "field": "acceptPickupTime", "value": true },
  "alternatives": [{ "placeId": "ChIJbloomCoffee2", "name": "Bloom Coffee", "address": "500 Howard St, San Francisco, CA" }]
}
```

Send exactly one of `choices` (the person's words) or `acceptPickupTime: true`, which accepts the time the decision states. You never type a time. When `answerWith` is `null`, there is no time to accept: build a new cart at one of `alternatives`, stores open now, if the person wants. Each answer adds to the ones before it, and together they must fit in 300 characters.

An answer takes its own `idempotencyKey`, by the same rules as a build. It starts a new cart: the `202` carries a new id, so poll that one. The question's cart then reads `closed` with `replacedBy`. Only the first key to answer starts a cart. Another key gets `409 answer_not_needed` with `error.replacedBy`.

## Pickup only

Every cart is a pickup. A request for delivery is built as a pickup, and `fulfillment.deliveryUnavailable.reason` is `not_enabled`, with a `message` you can show as written. The person can still confirm it.

## POST /v1/carts/:id/confirm

Run it only after the person has seen the cart and said yes. A live confirm may need the person's code (see [When the answer is code_required](#code-required)). In sandbox the order is simulated. Send an `idempotencyKey`, or the header, by the same rules as a build.

```bash
curl https://api.layout.link/v1/carts/crt_0d928d04f1iQbL1AV4iXSKw2wabMk_B-/confirm \
  -H "Authorization: Bearer $LAYOUT_PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "expectedTotalMinor": 763, "idempotencyKey": "7f3c2a9e-confirm-1" }'
```

```
202 Accepted

{
  "order": {
    "id": "ord_7c21e4b9a0d3",
    "status": "placing",
    "trackUrl": "https://layout.link/c/A7bK9mQz2xR"
  }
}
```

**Confirm never answers `placed`.** `placing` means the confirm was accepted. The order can still fail, so follow it with `GET /v1/orders/:id` or the `order.placed`, `order.failed` and `order.unconfirmed` webhooks. Treat `unconfirmed` as pending, never as failed: retrying it can charge the person twice. The pickup time can arrive later, with `order.updated` (see [Pickup time](https://docs.layout.link/api#pickup-time)). For the texts Layout sends the person, see [Texts Layout sends](https://docs.layout.link/mcp#texts-layout-sends).

**If the confirm times out, send the same call again with the same key.** Never a new key. The retry gets the first call's answer, or `409 in_progress` while it is still running. The person is charged once. See [Retrying a confirm](https://docs.layout.link/limits#retrying-a-confirm).

`expectedTotalMinor` must be the cart's `totalMinor`. The restaurant's total alone, on a ready cart with a service fee, is refused with `error.reason: "fee_not_included"`, usually as `400 invalid_request` and sometimes as `409 price_changed`. Any other number, including a total that moved, is `409 price_changed`: build a fresh cart with the same request and a new `idempotencyKey`, which carries the moved total, show the person that total, and confirm the fresh cart with it. Neither error carries a figure, and nothing is confirmed or charged.

`trackUrl` is Layout's live status page for the order, safe to show the person. It never shows the card or the account, and nothing can be signed in to or confirmed with it. It stops answering 15 minutes after the order ends, or 3 hours after the confirm at the latest. It is absent when Layout could not make one, and on every sandbox confirm.

### Already confirmed elsewhere

The person can confirm your cart without you: by replying YES to Layout's cart-ready text (see [If the card is missed](https://docs.layout.link/mcp#if-the-person-misses-the-card)), in the Layout app, or on Layout's tracking page. One confirm places the order. If it was already confirmed, your confirm answers `409 already_confirmed` and charges nothing again.

```
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 the order's state now, as `GET /v1/orders/:id` reports it. `unconfirmed` is not failed: do not build it again. `error.order.confirmedVia` is `sms` or `tracking_link` when Layout knows. `error.userMessage` can be shown as written. A retry of your own confirm with the same key still gets the answer it earned.

### When the answer is code_required

A live confirm needs the 6-digit code Layout texts the person only when [a code is due](https://docs.layout.link/how-it-works#when-a-code-is-due). Otherwise it answers `202 placing`. The cart's `codeRequired` and `codeReason`, checked on each read, say what to expect. The confirm still decides, so handle `code_required` on every confirm. The error carries the same `codeReason`.

| `codeReason` | What you can tell the person |
| --- | --- |
| `over_limit` | "Larger orders need a quick code." |
| `code_outstanding` | "Enter the code Layout texted you." |
| `first_order_consent` | "A one-time check for your first order with us." |
| `risk_check` | "Layout needs a quick code for this order." |
| `unverified` | Nothing yet. Read the cart again in a moment. |

In sandbox, only a cart built with ["simulate": "code"](https://docs.layout.link/sandbox#choose-the-ending) or at [test_place_code](https://docs.layout.link/sandbox#the-test-restaurants) asks for one: `codeReason` is `risk_check`, nothing is texted, and the code is `000000`.

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`. A code that expired, was used or replaced, or was texted for a different total is `400 code_expired`: send [resend-code](#resend-code). Eight wrong codes from your application for one person in 15 minutes refuse every code for them, the right one included, until the oldest is 15 minutes old. After three wrong codes for a number, tries are spaced apart, by up to 30 seconds. Both are `429 code_attempts_exceeded` with `Retry-After`. A number that replied STOP to Layout is `409 texts_blocked`: the person replies START to Layout's number, then you confirm again. Nothing is charged on any of these.

### Confirming with a build grant

A build grant confirms what the person said yes to in your product. Whether that needs their code, including the code on a first order (`first_order_consent`), is set out in [When a code is due](https://docs.layout.link/how-it-works#when-a-code-is-due).

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: `Layout: 482913 approves $7.63 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. Disconnect it 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, in Layout's words. Nothing your application sends appears in it. Only a `first_order_consent` code names your application (its name in the console) and says that sharing the code lets it order without one.
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 good, for 10 minutes; resend-code texts a fresh one. A text that could not be sent is `502 send_failed`: retry shortly. Layout texts at most four codes per cart, and your application has a [daily code allowance](https://docs.layout.link/limits#order-codes). Past either, the answer is `429 rate_limited` and nothing is texted. A code works once, for this cart and this total. 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`.

Until the person has a Layout account and a card, the confirm answers `403 build_only` and no code is texted. `error.next.step`, when present, names the missing step. See [When a card is needed](https://docs.layout.link/provisioning#adding-a-card).

| `error.next.step` | What you do |
| --- | --- |
| `verify_required` | Text them a code with `POST /v1/users/:id/verify/start`, and send the code they read back to `POST /v1/users/:id/verify`, which makes or joins their account. |
| `connect_required` | The person signs in to Layout on this number. Send a connect link with `POST /v1/users/:id/connect` for them to approve. |
| `card_required` | Mint a [card link](https://docs.layout.link/card-links) with `POST /v1/users/:id/links` and send them to it. For a person you provisioned there is no `url`. |
| `phone_required` | They verify their number in Layout. |

```
403 Forbidden

{
  "error": {
    "code": "build_only",
    "message": "This person has not verified their number with your application yet, so the cart cannot be confirmed. Text them a code with POST /v1/users/{userId}/verify/start, send the code they read back to POST /v1/users/{userId}/verify, then confirm again. Nothing was charged.",
    "orderId": "ord_7c21e4b9a0d3",
    "next": { "step": "verify_required" }
  }
}
```

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

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

Texts the person a fresh code, replacing any code already out for this cart, and answers `200 { "codeSent": true }`. Use it when the code never arrived or expired.

- `409 code_not_needed`: the cart needs no code. Nothing is texted.
- `409 already_confirmed`: the cart was confirmed, and the answer names the order.
- `409 texts_blocked`: the number replied STOP.
- `429 rate_limited`: past the code limits. `Retry-After` is set when waiting will help.
- `502 send_failed`: the text failed. Retry shortly.

In sandbox every cart answers `code_not_needed`, except one built with `"simulate": "code"` or at `test_place_code`, which answers `200` without texting anyone and meets production's code limits.

## POST /v1/carts/:id/cancel

A connected person's token, or a build grant for a cart your application built. Releases this cart 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. An expired cart needs no cancel; a cancel of one answers `200` with status `expired`.

A cart still building answers `202 { "cart": { "id": "crt_…", "status": "canceling" } }`. Layout releases it the moment the build lands, never texts the person about it, and sends `order.failed` (`USER_CANCELED`), never `order.carted`. Read the cart until it is `canceled`, or `failed`. A confirm on a `canceling` or `canceled` cart is `409 cart_closed`.

A cart already confirmed, here or elsewhere, is not canceled. The answer is `409 cart_closed` with `error.order`, the order and its state now. If that order already ended, its status is `failed`. A cart that already ended answers `409 nothing_to_cancel`.

```
409 {
  "error": {
    "code": "cart_closed",
    "message": "This cart was already confirmed, so it was not canceled. Read its order.",
    "orderId": "ord_7c21e4b9a0d3",
    "order": { "id": "ord_7c21e4b9a0d3", "status": "placing" }
  }
}
```

A cancel that releases a cart is never rate limited. See [Limits](https://docs.layout.link/limits#per-minute-limits).

## 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

In sandbox, a cart at a real restaurant is built from that restaurant's menu as Layout holds it, with its real items and choices. No browser opens. An item the menu does not list fails the cart with `ITEM_UNAVAILABLE`, and a restaurant whose menu Layout does not hold is `409 menu_not_held`, so build with a `placeId` from a sandbox places search. The confirm is simulated: nothing is ordered, charged or texted. It answers `202 placing` with `"simulated": true`, and the order then reaches the ending `simulate` asked for, `placed` by default. Every detail is in [Sandbox](https://docs.layout.link/sandbox).

## Errors

Every error has one shape, and every code a cart call can answer is listed in [Errors and status](https://docs.layout.link/errors#every-error-code).
