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

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

placeIdWhat it does
test_place_cartedBuilds a ready cart priced from the test menu. Its confirm places the order at once, simulated.
test_place_needs_choicesThe 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_locationMatches 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_southThe two branches. Each builds a ready cart like test_place_carted.
test_place_closedIs closed. The cart fails with failure code STORE_CLOSED.
test_place_price_changedBuilds 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_placedBuilds a ready cart. Its confirm answers placing, and the order is placed about 5 seconds later.
test_place_failedBuilds a ready cart. Its confirm answers placing, and the order fails about 5 seconds later with failure code CARD_DECLINED.
test_place_unconfirmedBuilds 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 says.
test_place_challengeBuilds 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_slowThe 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.

ItemPriceOptions
Drip coffee$3.50None
Latte$5.25Size: Small, Medium (+$0.50), Large (+$1.00). Extras: Oat milk (+$0.75), Extra shot (+$1.00), Vanilla (+$0.50).
Blueberry muffin$3.75None
Avocado toast$9.00None
Breakfast burrito$10.95None
  • 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.

placeIdEvents, in order
test_place_cartedorder.building, order.carted, then order.placed at confirm
test_place_closedorder.building, order.failed
test_place_placedorder.building, order.carted, order.placing, order.placed
test_place_failedorder.building, order.carted, order.placing, order.failed
test_place_unconfirmedorder.building, order.carted, order.placing, order.unconfirmed
test_place_challengeorder.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.

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.

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.

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.