Event types
The full catalog of webhook events. The vocabulary carries the truth: order.unconfirmed is first-class, and must never be collapsed into order.failed.
| Event | Meaning |
|---|---|
order.building | The order was accepted and the cart is being built. |
order.carted | The cart is priced and ready. Carries the store's name and address, how many items it holds, and the store's total, without Layout's service fee. A cart you canceled while it was building is never sent as order.carted: it ends with one order.failed carrying USER_CANCELED. |
order.placing | The person confirmed; Layout is submitting to the merchant. |
order.placed | The order reached the merchant, with evidence. Fires only on a confirmed placement. |
order.failed | Not placed. order.failure_code says why. A ready cart nobody confirmed before expiresAt is CART_EXPIRED. A failed order can still show a pending hold: see Holds on a failed order. |
order.unconfirmed | Placement is genuinely uncertain. Do not retry it, automatically or by hand, and do not treat it as failed. Wait for order.placed or read GET /v1/orders/:id. That is the double-charge trap. Layout keeps following the order, and order.placed can follow once it learns the outcome, for instance when the restaurant's receipt arrives. |
order.updated | More pickup details arrived after placement (name, time, order number, instructions). It can come several times, each with its own event_id: see More than one order.updated. Read the order again on each one. order.state stays placed, and it is sent only for an order whose order.placed was sent to you. |
order.refunded | Money went back to the person. order.refunded_minor is this one refund and order.method says where it went. order.state does not change. See order.refunded. Add it to a subscription’s events to receive it. |
order.challenge | Layout texted the person a link to approve this purchase, and it is waiting on them. A pause, not a new state: order.state is where the order was when it paused, which is building, carted or placing. Sent once per order, and only while the order is still in flight. |
order.needs_approval | Reserved. Not sent. |
Example payload
Every order event carries the same payload, whatever its type, on a webhook and in GET /v1/events. It never carries item names, modifiers or card data: read GET /v1/carts/:id for a cart's items and GET /v1/orders/:id for the order.
{
"event": "order.carted",
"event_id": "evt_7c21e4b9a0d3_carted",
"delivery_id": "dl_351eacf74201",
"order": {
"id": "ord_7c21e4b9a0d3",
"state": "carted",
"store": "Layout test kitchen",
"store_address": "1 Sandbox Way, San Francisco, CA 94107",
"items": 2,
"total_minor": 2140,
"currency": "usd"
},
"user": { "id": "usr_4b8e9a01c2d3" },
"occurred_at": "2026-09-10T17:01:05Z",
"sent_at": "2026-09-10T17:01:07Z"
}
| Field | Notes |
|---|---|
event | The event type (not type). |
event_id | The event: this order reaching this state, or one wave of order.updated. Every delivery of it, to every endpoint, on every retry and replay, carries the same id. Deduplicate on it and treat it as opaque. |
delivery_id | This delivery (not id). A retry keeps it; a replay gets a new one. |
order.id | The same ord_ id GET /v1/orders/:id returns. The order is nested under order (not data). |
order.store, order.store_address | The store's name and address. store_address is null when Layout does not know it, and always null when store is. |
order.items | How many items the order holds. |
order.total_minor | The store's total in minor units, without Layout's service fee, the same as on GET /v1/orders/:id. Null until the cart is priced. The cart's totalMinor includes the fee, and the order's charged_minor is what the card was charged. |
order.failure_code | On order.failed only: the same code GET /v1/orders/:id returns as failure_code, or null when Layout recorded none. |
order.ready_eta, order.ready_at, order.fulfillment | On order.updated only: the values GET /v1/orders/:id returns when the event is written. ready_at is the pickup time as an instant, or null when Layout cannot pin the store’s words to its clock. Absent when Layout could not read the order then. |
order.refunded_minor, order.method | On order.refunded only: the amount of this one refund in minor units, and card or credit. |
order.simulated | true on a simulated sandbox order. See Simulated orders. |
test | true on a console test delivery, a fired test event and endpoint.verification. |
user.id | The usr_ id you provisioned. For a person who connected through your OAuth client, a usr_ id that is stable for your application and differs in every other one; POST /v1/users/:id/grant does not accept it. |
occurred_at, sent_at | When the order changed, and when Layout wrote the delivery (kept on a retry, restamped on a replay). Order events by occurred_at, never by arrival. |
simulated and test are absent, never false, when not true. endpoint.verification has its own shape with no order or user: see Endpoint verification.
order.refunded
Sent for each refund on an order. It changes no state, so an order that was placed stays placed; the deprecated refunded state is never sent. order.refunded_minor is the amount of this refund, in minor units, and order.method is card when it goes back to the person’s card, or credit when it is Layout credit on their account. GET /v1/orders/:id refunded_minor is the total refunded so far. A second refund has its own event_id, ending _2, so de-duplicate on event_id.
It is not part of any subscription you already have: edit the subscription and add it to its events.
More than one order.updated
Every other event is sent at most once per order. order.updated is sent once for each wave of pickup details that fills a detail the order did not have yet. A detail already there is never replaced.
| Wave | event_id |
|---|---|
| First | evt_7c21e4b9a0d3_updated |
| Second | evt_7c21e4b9a0d3_updated_2 |
| Third, and so on | evt_7c21e4b9a0d3_updated_3 |
- De-duplicating on
event_idkeeps every wave, because each has its own id. A retry or a replay of one wave keeps that wave's id. - Do not key on the order id and the event type together: that drops the second wave and everything after it.
GET /v1/events?after=evt_7c21e4b9a0d3_updated_2resumes right after that wave.- On each one, read
GET /v1/orders/:idand show what it has now. The pickup time is often the last detail to arrive: see Pickup time. - Test it with
test_place_pickup_later, which sends two.
Which orders send events
Events fire for orders your application caused, and only those:
- Orders built for a user you provisioned, through a build session from
POST /v1/users/:id/grant. - Orders a person placed through an OAuth client you created in the console, while they are still connected to it.
An order a person placed on their own, or through anyone else's app, never reaches you. Layout sends no event when a person is disconnected.
Simulated orders
A simulated sandbox order carries "simulated": true in its order object: nothing went to the restaurant and nothing was charged. A sandbox order built from the menu Layout holds, or at a test restaurant, carries it on every event, from order.building on. A sandbox order built with "engine": "real" carries it once its confirm is simulated, and not on its events before that. On every other delivery the key is absent, never false, so test for true. GET /v1/orders/:id follows the same rule.