# Sandbox test restaurants

Each test restaurant scripts one outcome, the way a payment processor's test cards do, so you can test every path your integration handles in CI. They never open a browser, cost nothing, and never count toward your sandbox build allowance.

## Who can use them

Sandbox credentials only: a sandbox build grant, or a connected person's token where the person is a sandbox test user, such as a [sign-in test account](https://developer.layout.link/reference/authentication#sign-in-test-accounts). Name a test restaurant by its `placeId`, which always starts `test_place_`.

Over MCP that means a session started from a sandbox build grant. An OAuth connector session never reaches them, even when the person it signs in is a test user.

A production credential that names a test restaurant gets exactly the answer it would get for any place that does not exist: `store_not_found` from `POST /v1/carts`, a 404 from the place and menu reads, and an unknown store from the MCP `order` tool. Nothing is built.

## Find them

In sandbox, a search that names them lists them, up to `limit` (5 by default, at most 10) like any search. Add a word to narrow it: `layout test slow` finds `test_place_slow`. None of these reads draws your place or menu allowance.

```bash
curl "https://api.layout.link/v1/places?query=layout%20test" \
  -H "Authorization: Bearer $LAYOUT_PERSON_TOKEN"
```

`GET /v1/places/test_place_carted` reads one, and `POST /v1/menus` with a test `placeId` returns the test menu. Over MCP, pass the `placeId` straight to the `order` tool's `build` action; the `places` and `menu` tools do not list test restaurants.

## The test restaurants

| placeId | What it does |
| --- | --- |
| `test_place_carted` | Builds a ready cart priced from the test menu. Its confirm places the order at once, simulated. |
| `test_place_needs_choices` | The cart asks a `choices` decision: Size (required: Small, Medium, Large) and Extras (optional: Oat milk, Extra shot, Vanilla). It asks again until the answer names a size, then builds a ready cart for a latte. |
| `test_place_needs_location` | Matches two branches. `POST /v1/carts` answers `store_ambiguous` with both as `candidates`; over MCP the build asks which one. |
| `test_place_branch_north`, `test_place_branch_south` | The two branches. Each builds a ready cart like `test_place_carted`. |
| `test_place_closed` | Is closed. The cart fails with failure code `STORE_CLOSED`. |
| `test_place_price_changed` | Builds a ready cart. At the first confirm its total goes up $1.00, so that confirm is refused with `price_changed`. Read the cart again and confirm its new total. Over MCP, where the refusal asks for a fresh cart, build `test_place_price_changed` again within 15 minutes, while the moved cart is still unconfirmed: the fresh cart starts at the new total and its confirm places it. |
| `test_place_placed` | Builds a ready cart. Its confirm answers `placing`, and the order is placed about 5 seconds later. |
| `test_place_failed` | Builds a ready cart. Its confirm answers `placing`, and the order fails about 5 seconds later with failure code `CARD_DECLINED`. |
| `test_place_unconfirmed` | Builds a ready cart. Its confirm answers `placing`, and about 5 seconds later the order ends `unconfirmed`. Handle it as you would in production: never build it again, and never tell the person it failed. Over MCP, `order_status` reports it as `failed` with `outcome: "unconfirmed"` and an instruction not to retry it. Treat that as unconfirmed, as [An unconfirmed placement](https://developer.layout.link/reference/mcp#an-unconfirmed-placement) says. |
| `test_place_challenge` | Builds a ready cart. Its confirm answers `placing` and sends `order.challenge`. About 15 seconds later the challenge clears by itself and the order is placed. |
| `test_place_slow` | The cart stays `building` for about 20 seconds, then becomes ready. |

Any other id that starts `test_place_` is reserved and finds nothing.

## The test menu

Every test restaurant shares one menu, so a total is the same every time you build it.

| Item | Price | Options |
| --- | --- | --- |
| Drip coffee | $3.50 | None |
| Latte | $5.25 | Size: Small, Medium (+$0.50), Large (+$1.00). Extras: Oat milk (+$0.75), Extra shot (+$1.00), Vanilla (+$0.50). |
| Blueberry muffin | $3.75 | None |
| Avocado toast | $9.00 | None |
| Breakfast burrito | $10.95 | None |

- Each entry of `items`, or each comma in `request`, is one line. A leading number is the quantity: `"2 lattes"`.
- An option named anywhere in the request applies to the items that offer it. A latte with no size named is Small, except at `test_place_needs_choices`, which asks.
- Anything the menu does not list is priced at $5.00 under the name you sent.
- Tax is 8.75% of the subtotal. The service fee is the same as in production, and `totalMinor` includes it.

## What they write

A test restaurant writes the same order a real build does, marked `simulated: true`. `GET /v1/carts/:id`, confirm, `GET /v1/orders/:id`, the event feed and your webhooks all treat it as an order, and every event it sends carries `"simulated": true`. No card is shown and no code is asked for.

| placeId | Events, in order |
| --- | --- |
| `test_place_carted` | `order.building`, `order.carted`, then `order.placed` at confirm |
| `test_place_closed` | `order.building`, `order.failed` |
| `test_place_placed` | `order.building`, `order.carted`, `order.placing`, `order.placed` |
| `test_place_failed` | `order.building`, `order.carted`, `order.placing`, `order.failed` |
| `test_place_unconfirmed` | `order.building`, `order.carted`, `order.placing`, `order.unconfirmed` |
| `test_place_challenge` | `order.building`, `order.carted`, `order.placing`, `order.challenge`, `order.placed` |

A question (`test_place_needs_choices` before it is answered, `test_place_needs_location`) writes no order and sends nothing. A ready cart nobody confirms expires as it would in production.

The times are approximate. An order moves on within a few seconds of being due. Reading the order, with `GET /v1/orders/:id` or `order_status` over MCP, moves it on the moment it is due. Reading a cart moves on only a slow build.

## A whole order in CI

Give every run its own idempotency keys, for example from your CI run id. A key reused from an earlier run replays that run's cart and confirm instead of building a new one.

```bash
curl https://api.layout.link/v1/carts \
  -H "Authorization: Bearer $LAYOUT_PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "placeId": "test_place_placed", "items": ["latte", "blueberry muffin"], "idempotencyKey": "ci-'"$RUN_ID"'-cart" }'
```

```
202 Accepted

{ "cart": { "id": "crt_3f9a1c0b7dY2ktcnVuLTAwMDE", "status": "building" } }
```

Read the cart until it is `ready`, confirm it with its `totalMinor`, then read the order or wait for `order.placed`.

```bash
curl https://api.layout.link/v1/carts/crt_3f9a1c0b7dY2ktcnVuLTAwMDE/confirm \
  -H "Authorization: Bearer $LAYOUT_PERSON_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "expectedTotalMinor": 1019, "idempotencyKey": "ci-'"$RUN_ID"'-confirm" }'
```

```
202 Accepted

{ "order": { "id": "ord_7c21", "status": "placing", "simulated": true } }
```

## Limits

Test restaurant builds have their own allowance: 10,000 a day for each developer account, across all its applications, inside a pool shared by every developer. They never draw your sandbox build allowance, and they do not appear in Usage. A refusal is `build_limit` and names the number. Both reset at midnight UTC.

## Fire a test webhook

`POST /v1/sandbox/webhooks/fire` sends one signed test event to every enabled endpoint of your application that subscribes to it, so you can test a handler without building anything. It goes through the real delivery pipeline: the sandbox signing secret, the same headers, the delivery log and the retries. Call it with your `sk_test_` key.

```bash
curl https://api.layout.link/v1/sandbox/webhooks/fire \
  -H "Authorization: Bearer $LAYOUT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "event": "order.unconfirmed", "orderId": "ord_7c21" }'
```

```
200 OK

{
  "event": "order.unconfirmed",
  "deliveries": [
    { "id": "dl_9f3a2c", "url": "https://example.com/layout/webhooks", "outcome": "delivered" }
  ]
}
```

- `event` is any event type except `order.needs_approval`, which is reserved and never sent.
- `orderId` is optional. With one of your sandbox orders, the event describes that order. Without it, the event describes a test order whose ids match nothing.
- Every fired event carries `"test": true`, like the console's **Send test event**.
- `outcome` is `delivered` when your endpoint answered 2xx, `retry` when it did not and the ordinary retry schedule takes over, and `dead` when retries are exhausted.
- A `sk_live_` key is refused with 403 `forbidden`. No endpoint subscribed to the event, or an `orderId` that is not one of your sandbox orders, is a 404 `not_found` that says which.
- 1,000 a day for each developer account and 30 a minute for each key, refused with 429 `rate_limited`.
