Skip to content

Event catalog

These are the event types you can subscribe to. Each event uses the standard envelope; the examples below show the full envelope with a representative data.object.

Handle unknown event types gracefully — new types may be added over time.

Event When it fires
reservation.created A reservation is created, or on a guaranteed slot once its card is saved — its status is confirmed (auto-confirmed) or pending (awaiting approval).
reservation.pending A confirmed reservation was changed, by the guest or by a PATCH, in a way that needs re-approval.
reservation.confirmed A pending reservation is later approved. Auto-confirmed reservations do not fire this.
reservation.declined A pending reservation is declined.
reservation.updated A reservation’s details change (party size, date, notes, …).
reservation.partially_seated Some, but not all, of the party has been seated.
reservation.seated The party has been seated.
reservation.completed The reservation has finished (the party has left).
reservation.cancelled The reservation is cancelled.
reservation.no_show The party did not arrive.
guest.created A guest record is created.
guest.updated A guest’s details change.
guest.deleted A guest is deleted (e.g. data-protection erasure).
guest.merged Two guest profiles were merged. The absorbed id now resolves to the survivor.
guest.unmerged A merge was undone and the absorbed profile restored.
reservation.guarantee_requested A card guarantee was requested for a reservation.
reservation.guarantee_charged The card was charged — fully or partially.
reservation.guarantee_refunded A charge was refunded, fully or partially.
reservation.guarantee_released The guarantee ended with no charge.
reservation.guarantee_payment_failed A charge attempt failed.
feedback.received A guest submits feedback after a visit.
waitlist_entry.created A guest joined the waitlist.
waitlist_entry.promoted A waitlist entry became a reservation.
waitlist_entry.updated A guest changed their waitlist request — the window, the party size, or both.
waitlist_entry.cancelled A waitlist entry was withdrawn by the guest or the restaurant.
waitlist_entry.expired A waitlist entry ran out of time and was closed.
hold.created A slot was held. The table is occupied, and nobody has booked it.
hold.converted A hold became the reservation it names. Count it as the hold being released.
hold.released A hold was handed back before its deadline.
hold.expired A hold ran out of time.

The data.object of every reservation event is a full reservation, in the same shape as the API reference returns — the complete resource snapshot, not a diff, guest contact details and notes included (see Deliveries carry guest data). It always carries company: the company the booking is made for, as { "object": "company", "id": "cmp_…", "name": … }, or null.

Only reservation.updated carries data.previous_attributes; the other reservation events omit it. When present, previous_attributes contains only the changed fields among party_size, service_date, guest_notes, internal_notes, excluded_from_shift_limits, guest (the previous guest as a bare gst_ id) and company (the previous company object, or null when there was none). company is left out when the previous company has since been deleted. Time changes are reflected in the snapshot’s starts_at / ends_at, not in previous_attributes.

A reservation is created (via the widget, the back office, an integration, a waitlist promotion or a hold conversion). It fires once per booking — check data.object.status to tell them apart: confirmed means it was auto-confirmed at creation (no approval step; typical for back-office and default bookings), while pending means it needs approval and will later emit reservation.confirmed or reservation.declined.

Three kinds of booking do not fire it at creation:

  • A booking on a slot that carries a card guarantee. The event is withheld until the guest saves the card, until the restaurant accepts the booking without one, or until a PATCH takes the card requirement away, and it fires then. No other reservation.* event about the booking is sent before it. If the card is never saved and the booking is cancelled, reservation.created never fires, and neither does reservation.cancelled. See Guaranteed bookings arrive out of order.
  • A hold. Taking one fires hold.created, not this; converting it fires both. See A conversion fires two events.
  • A reservation imported from another booking system when the restaurant moved to Service. Imports fire no reservation.* webhook.
{
"id": "evt_1K8xQ2m4Vd0pErJ7sN1aZ9bQ",
"object": "event",
"type": "reservation.created",
"created": "2026-06-20T09:04:18Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "pending",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 4,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-20T11:04:18+02:00"
}
}
}

A reservation that was already confirmed has been changed in a way that needs the restaurant to approve it again — so it has returned to pending. The change can come from the guest on their own booking page, or from a PATCH /v1/reservations/{id} through this API: a bigger party, or a move to a service where the restaurant approves bookings.

A change to a booking that is already pending does not fire this event. It arrives as reservation.updated, and the booking stays pending.

{
"id": "evt_H8iJ9kL0mN1oP2qR3sT4uV5w",
"object": "event",
"type": "reservation.pending",
"created": "2026-07-14T09:22:31Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_DA2ipZ7Enk01lpIhd0saMQ1e",
"status": "pending",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 4,
"service_date": "2026-07-18",
"starts_at": "2026-07-18T20:00:00+02:00",
"ends_at": "2026-07-18T22:00:00+02:00",
"guest_notes": null,
"contact_email": "[email protected]",
"contact_phone": "+33612345678",
"internal_notes": null,
"guest": "gst_FD7tTyOA9c-ZpPpeF0XMhkZI",
"company": null,
"tables": [],
"guarantee": null,
"metadata": {},
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-14T11:22:31+02:00"
}
}
}

A pending reservation is approved — the manual-approval flow. This fires only for reservations created as pending; an auto-confirmed reservation never emits reservation.confirmed: it arrives once, as reservation.created with status: "confirmed".

{
"id": "evt_2M9yR3n5We1qFsK8tO2bA0cR",
"object": "event",
"type": "reservation.confirmed",
"created": "2026-06-20T09:30:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "confirmed",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 4,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-20T11:30:00+02:00"
}
}
}

A pending reservation is declined.

{
"id": "evt_3N0zS4o6Xf2rGtL9uP3cB1dS",
"object": "event",
"type": "reservation.declined",
"created": "2026-06-20T09:31:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "cancelled",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 4,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-20T11:31:00+02:00"
}
}
}

A reservation’s details change — for example party size or notes. previous_attributes lists the changed fields and their previous values (here, the party size grew from 4 to 6, the guest note changed, the restaurant wrote a note of its own, and the booking was put on a company’s account). A change to internal_notes alone fires this event like any other change.

{
"id": "evt_4O1aT5p7Yg3sHuM0vQ4dC2eT",
"object": "event",
"type": "reservation.updated",
"created": "2026-06-25T07:12:55Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "confirmed",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 6,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table; celebrating an anniversary",
"internal_notes": "Regulars — the corner banquette if it is free",
"guest": "gst_3Td9Lp0WqZ",
"company": {
"object": "company",
"id": "cmp_7Hk2Rw9XsB",
"name": "Atelier Morel"
},
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-25T09:12:55+02:00"
},
"previous_attributes": {
"party_size": 4,
"guest_notes": "Window table if possible",
"internal_notes": null,
"company": null
}
}
}

Some, but not all, of the party has been seated.

{
"id": "evt_5P2bU6q8Zh4tIvN1wR5eD3fU",
"object": "event",
"type": "reservation.partially_seated",
"created": "2026-06-27T18:05:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "partially_seated",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 6,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [
{
"object": "table",
"id": "tbl_QpZ2",
"name": "12",
"min_capacity": 2,
"max_capacity": 6,
"section": {
"object": "section",
"id": "sec_Lm8",
"name": "Main room",
"area_type": "indoor"
}
}
],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-27T20:05:00+02:00"
}
}
}

The party has been seated.

{
"id": "evt_6Q3cV7r9Ai5uJwO2xS6fE4gV",
"object": "event",
"type": "reservation.seated",
"created": "2026-06-27T18:08:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "seated",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 6,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [
{
"object": "table",
"id": "tbl_QpZ2",
"name": "12",
"min_capacity": 2,
"max_capacity": 6,
"section": {
"object": "section",
"id": "sec_Lm8",
"name": "Main room",
"area_type": "indoor"
}
}
],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-27T20:08:00+02:00"
}
}
}

The reservation has finished — the party has left.

{
"id": "evt_7R4dW8s0Bj6vKxP3yT7gF5hW",
"object": "event",
"type": "reservation.completed",
"created": "2026-06-27T20:15:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "completed",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 6,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-27T22:15:00+02:00"
}
}
}

The reservation is cancelled.

{
"id": "evt_8S5eX9t1Ck7wLyQ4zU8hG6iX",
"object": "event",
"type": "reservation.cancelled",
"created": "2026-06-26T16:00:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "cancelled",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 6,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-26T18:00:00+02:00"
}
}
}

The party did not arrive.

{
"id": "evt_9T6fY0u2Dl8xMzR5aV9iH7jY",
"object": "event",
"type": "reservation.no_show",
"created": "2026-06-27T18:45:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ",
"status": "no_show",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 6,
"service_date": "2026-06-27",
"starts_at": "2026-06-27T20:00:00+02:00",
"ends_at": "2026-06-27T22:00:00+02:00",
"guest_notes": "Window table if possible",
"internal_notes": null,
"guest": "gst_3Td9Lp0WqZ",
"company": null,
"tables": [],
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-27T20:45:00+02:00"
}
}
}

A card guarantee is a payment card held against a reservation — an imprint that is charged only if the guest does not honour the booking, under the restaurant’s own no-show policy.

These five ids are reservation.* because the data.object is the reservation, carrying the guarantee embedded under guarantee. But they are emitted from the guarantee’s own lifecycle rather than the reservation’s, so they do not appear in the GET /v1/reservations/{id}/events feed, which lists reservation events only. Webhooks are the way to observe them.

The money fields are integers in the currency’s minor unit — 5000 is €50.00.

A request that ends before any card is saved goes to state: "withdrawn" and fires none of the five: the booking was cancelled, the restaurant withdrew the request, or a change made the card unnecessary. Nothing was charged. You read the state on the reservation.

The restaurant asked the guest for a card on a booking that already exists. The booking goes back to awaiting_guarantee, the guarantee’s state is awaiting_card, and no money has moved. origin is staff_request, card and consented_at are null until the guest saves a card, and expires_at is the payment deadline.

A create through this API on a guaranteed slot does not fire this event. That booking is not published until the card is saved or the request is withdrawn, so you learn about it from reservation.created, with the guarantee already active, or withdrawn when a PATCH took the card requirement away. Its guarantee’s origin is staff_request too: the card is asked for by the same pay-link.

{
"id": "evt_L1mN2oP3qR4sT5uV6wX7yZ8a",
"object": "event",
"type": "reservation.guarantee_requested",
"created": "2026-07-11T09:12:04Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_1M__exJ2ZQ7jKENZxz28djsH",
"status": "awaiting_guarantee",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 2,
"service_date": "2026-07-18",
"starts_at": "2026-07-18T20:00:00+02:00",
"ends_at": "2026-07-18T22:00:00+02:00",
"guest_notes": null,
"contact_email": "[email protected]",
"contact_phone": "+33612345678",
"internal_notes": null,
"guest": "gst_e_I4WiTGNl0S1v7W2G6M6ygk",
"company": null,
"tables": [],
"guarantee": {
"object": "guarantee",
"kind": "imprint",
"state": "awaiting_card",
"origin": "staff_request",
"amount": 5000,
"currency": "EUR",
"charged_amount": 0,
"refunded_amount": 0,
"card": null,
"cancel_deadline_at": "2026-07-17T20:00:00+02:00",
"expires_at": "2026-07-13T11:12:04+02:00",
"consented_at": null,
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-11T11:12:04+02:00"
},
"metadata": {},
"created_at": "2026-07-09T18:40:12+02:00",
"updated_at": "2026-07-11T11:12:04+02:00"
}
}
}

The card was charged.

{
"id": "evt_M2nO3pQ4rS5tU6vW7xY8zA9b",
"object": "event",
"type": "reservation.guarantee_charged",
"created": "2026-07-18T20:31:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_1M__exJ2ZQ7jKENZxz28djsH",
"status": "no_show",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 2,
"service_date": "2026-07-18",
"starts_at": "2026-07-18T20:00:00+02:00",
"ends_at": "2026-07-18T22:00:00+02:00",
"guest_notes": null,
"contact_email": "[email protected]",
"contact_phone": "+33612345678",
"internal_notes": null,
"guest": "gst_e_I4WiTGNl0S1v7W2G6M6ygk",
"company": null,
"tables": [],
"guarantee": {
"object": "guarantee",
"kind": "imprint",
"state": "charged",
"origin": "online_booking",
"amount": 5000,
"currency": "EUR",
"charged_amount": 5000,
"refunded_amount": 0,
"card": { "brand": "visa", "last4": "4242" },
"cancel_deadline_at": "2026-07-17T20:00:00+02:00",
"expires_at": null,
"consented_at": "2026-07-11T11:12:04+02:00",
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
},
"metadata": {},
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
}
}
}

A charge was refunded, in whole or in part. refunded_amount is the cumulative total refunded, not the amount of this refund — a second partial refund raises it again rather than emitting a delta.

{
"id": "evt_N3oP4qR5sT6uV7wX8yZ9aB0c",
"object": "event",
"type": "reservation.guarantee_refunded",
"created": "2026-07-19T08:00:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_1M__exJ2ZQ7jKENZxz28djsH",
"status": "no_show",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 2,
"service_date": "2026-07-18",
"starts_at": "2026-07-18T20:00:00+02:00",
"ends_at": "2026-07-18T22:00:00+02:00",
"guest_notes": null,
"contact_email": "[email protected]",
"contact_phone": "+33612345678",
"internal_notes": null,
"guest": "gst_e_I4WiTGNl0S1v7W2G6M6ygk",
"company": null,
"tables": [],
"guarantee": {
"object": "guarantee",
"kind": "imprint",
"state": "refunded",
"origin": "online_booking",
"amount": 5000,
"currency": "EUR",
"charged_amount": 5000,
"refunded_amount": 2500,
"card": { "brand": "visa", "last4": "4242" },
"cancel_deadline_at": "2026-07-17T20:00:00+02:00",
"expires_at": null,
"consented_at": "2026-07-11T11:12:04+02:00",
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
},
"metadata": {},
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
}
}
}

The guarantee ended without a charge — the guest honoured the booking, or the restaurant released the hold. Nothing was taken; charged_amount is 0.

{
"id": "evt_O4pQ5rS6tU7vW8xY9zA0bC1d",
"object": "event",
"type": "reservation.guarantee_released",
"created": "2026-07-18T22:30:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_1M__exJ2ZQ7jKENZxz28djsH",
"status": "completed",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 2,
"service_date": "2026-07-18",
"starts_at": "2026-07-18T20:00:00+02:00",
"ends_at": "2026-07-18T22:00:00+02:00",
"guest_notes": null,
"contact_email": "[email protected]",
"contact_phone": "+33612345678",
"internal_notes": null,
"guest": "gst_e_I4WiTGNl0S1v7W2G6M6ygk",
"company": null,
"tables": [],
"guarantee": {
"object": "guarantee",
"kind": "imprint",
"state": "released",
"origin": "online_booking",
"amount": 5000,
"currency": "EUR",
"charged_amount": 0,
"refunded_amount": 0,
"card": { "brand": "visa", "last4": "4242" },
"cancel_deadline_at": "2026-07-17T20:00:00+02:00",
"expires_at": null,
"consented_at": "2026-07-11T11:12:04+02:00",
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
},
"metadata": {},
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
}
}
}

A charge attempt failed — an expired card, a decline, insufficient funds. The guarantee stays chargeable until its window closes, so this is not necessarily terminal: a later attempt may succeed and emit reservation.guarantee_charged.

{
"id": "evt_P5qR6sT7uV8wX9yZ0aB1cD2e",
"object": "event",
"type": "reservation.guarantee_payment_failed",
"created": "2026-07-18T20:31:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "reservation",
"id": "resv_1M__exJ2ZQ7jKENZxz28djsH",
"status": "no_show",
"source": "widget",
"booking_channel": "website_widget",
"party_size": 2,
"service_date": "2026-07-18",
"starts_at": "2026-07-18T20:00:00+02:00",
"ends_at": "2026-07-18T22:00:00+02:00",
"guest_notes": null,
"contact_email": "[email protected]",
"contact_phone": "+33612345678",
"internal_notes": null,
"guest": "gst_e_I4WiTGNl0S1v7W2G6M6ygk",
"company": null,
"tables": [],
"guarantee": {
"object": "guarantee",
"kind": "imprint",
"state": "charge_failed",
"origin": "online_booking",
"amount": 5000,
"currency": "EUR",
"charged_amount": 0,
"refunded_amount": 0,
"card": { "brand": "visa", "last4": "4242" },
"cancel_deadline_at": "2026-07-17T20:00:00+02:00",
"expires_at": null,
"consented_at": "2026-07-11T11:12:04+02:00",
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
},
"metadata": {},
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T22:31:00+02:00"
}
}
}

The data.object of a guest event is a full guest, in the same shape as the API reference returns; its notes are the restaurant staff’s notes on the guest profile. Guest events do not carry previous_attributes — compare against your own stored copy if you need a diff.

A guest has a first name, a last name, or both: first_name or last_name can be null, never both. A guest known by a single name keeps it in the field it was recorded in.

A guest record is created.

{
"id": "evt_A1bC2dE3fG4hI5jK6lM7nO8p",
"object": "event",
"type": "guest.created",
"created": "2026-06-20T09:04:18Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "guest",
"id": "gst_3Td9Lp0WqZ",
"first_name": "Marie",
"last_name": "Dupont",
"email": "[email protected]",
"phone": "+33612345678",
"notes": null,
"blacklisted": false,
"blacklist_reason": null,
"dietary_preferences": [],
"allergies": [],
"birthday": null,
"anniversary": null,
"vip": false,
"language": "fr",
"source": "widget",
"marketing_email_consent": true,
"marketing_email_consent_at": "2026-06-20T11:04:18+02:00",
"marketing_sms_consent": false,
"marketing_sms_consent_at": null,
"visit_count": 0,
"no_show_count": 0,
"cancellation_count": 0,
"first_visit_at": null,
"last_visit_at": null,
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-20T11:04:18+02:00"
}
}
}

A guest’s details change. (Visit statistics alone do not trigger this event — see Pagination.)

{
"id": "evt_B2cD3eF4gH5iJ6kL7mN8oP9q",
"object": "event",
"type": "guest.updated",
"created": "2026-06-28T08:00:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "guest",
"id": "gst_3Td9Lp0WqZ",
"first_name": "Marie",
"last_name": "Dupont",
"email": "[email protected]",
"phone": "+33612345678",
"notes": null,
"blacklisted": false,
"blacklist_reason": null,
"dietary_preferences": ["vegetarian"],
"allergies": [],
"birthday": null,
"anniversary": null,
"vip": true,
"language": "fr",
"source": "widget",
"marketing_email_consent": true,
"marketing_email_consent_at": "2026-06-20T11:04:18+02:00",
"marketing_sms_consent": false,
"marketing_sms_consent_at": null,
"visit_count": 3,
"no_show_count": 0,
"cancellation_count": 0,
"first_visit_at": "2026-06-21T21:00:00+02:00",
"last_visit_at": "2026-06-27T20:00:00+02:00",
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-28T10:00:00+02:00"
}
}
}

A guest is deleted (for example, a data-protection erasure request). The data.object is a final snapshot of the full guest (the same shape as any other guest payload); the guest is no longer retrievable from the API afterwards.

{
"id": "evt_C3dE4fG5hI6jK7lM8nO9pQ0r",
"object": "event",
"type": "guest.deleted",
"created": "2026-06-29T07:00:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "guest",
"id": "gst_3Td9Lp0WqZ",
"first_name": "Marie",
"last_name": "Dupont",
"email": "[email protected]",
"phone": "+33612345678",
"notes": null,
"blacklisted": false,
"blacklist_reason": null,
"dietary_preferences": ["vegetarian"],
"allergies": [],
"birthday": null,
"anniversary": null,
"vip": true,
"language": "fr",
"source": "widget",
"marketing_email_consent": true,
"marketing_email_consent_at": "2026-06-20T11:04:18+02:00",
"marketing_sms_consent": false,
"marketing_sms_consent_at": null,
"visit_count": 3,
"no_show_count": 0,
"cancellation_count": 0,
"first_visit_at": "2026-06-21T21:00:00+02:00",
"last_visit_at": "2026-06-27T20:00:00+02:00",
"created_at": "2026-06-20T11:04:18+02:00",
"updated_at": "2026-06-28T10:00:00+02:00"
}
}
}

Two guest profiles were merged — usually because the same person was recorded twice. The data.object is the surviving guest.

data.previous_attributes is not used here. The absorbed profile’s id is not in the payload: fetch the survivor if you need the merged state, and note that the absorbed id keeps resolving — GET /v1/guests/{absorbed_id} returns the survivor rather than a 404, so references you already hold stay valid.

{
"id": "evt_J9kL0mN1oP2qR3sT4uV5wX6y",
"object": "event",
"type": "guest.merged",
"created": "2026-07-02T14:05:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "guest",
"id": "gst_e_I4WiTGNl0S1v7W2G6M6ygk",
"first_name": "Marie",
"last_name": "Dupont",
"email": "[email protected]",
"phone": "+33612345678",
"notes": null,
"blacklisted": false,
"blacklist_reason": null,
"dietary_preferences": [],
"allergies": [],
"birthday": null,
"anniversary": null,
"vip": false,
"language": "fr",
"source": "manual",
"marketing_email_consent": false,
"marketing_email_consent_at": null,
"marketing_sms_consent": false,
"marketing_sms_consent_at": null,
"visit_count": 4,
"no_show_count": 0,
"cancellation_count": 0,
"first_visit_at": "2026-05-02T20:00:00+02:00",
"last_visit_at": "2026-06-27T20:00:00+02:00",
"created_at": "2026-05-01T10:00:00+02:00",
"updated_at": "2026-07-02T16:05:00+02:00"
}
}
}

A merge was undone and the absorbed profile restored as a separate guest. The data.object is the restored guest — the one that had been absorbed.

After this, the restored id resolves to itself again rather than to the survivor. If you cached the mapping from guest.merged, drop it.

{
"id": "evt_K0lM1nO2pQ3rS4tU5vW6xY7z",
"object": "event",
"type": "guest.unmerged",
"created": "2026-07-03T09:30:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "guest",
"id": "gst_FD7tTyOA9c-ZpPpeF0XMhkZI",
"first_name": "Marie",
"last_name": "Dupond",
"email": "[email protected]",
"phone": null,
"notes": null,
"blacklisted": false,
"blacklist_reason": null,
"dietary_preferences": [],
"allergies": [],
"birthday": null,
"anniversary": null,
"vip": false,
"language": "fr",
"source": "widget",
"marketing_email_consent": false,
"marketing_email_consent_at": null,
"marketing_sms_consent": false,
"marketing_sms_consent_at": null,
"visit_count": 1,
"no_show_count": 0,
"cancellation_count": 0,
"first_visit_at": "2026-06-01T20:00:00+02:00",
"last_visit_at": "2026-06-01T20:00:00+02:00",
"created_at": "2026-06-01T18:00:00+02:00",
"updated_at": "2026-07-03T11:30:00+02:00"
}
}
}

A guest submits feedback after a visit. The data.object is an embedded feedback value-object (it is not a retrievable API resource) carrying the ratings and comment, plus bare-id references to the guest and the reservation it relates to.

{
"id": "evt_D4eF5gH6iJ7kL8mN9oP0qR1s",
"object": "event",
"type": "feedback.received",
"created": "2026-06-28T11:20:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "feedback",
"id": "fb_7hn2qp",
"overall_rating": 5,
"food_rating": 5,
"service_rating": 4,
"comment": "Lovely evening, great service.",
"responded_at": "2026-06-28T13:20:00+02:00",
"guest": "gst_3Td9Lp0WqZ",
"reservation": "resv_8xKQ2m4Vd0pErJ7sN1aZ9bQ"
}
}
}

The overall_rating, food_rating, service_rating, and comment fields may each be null when the guest did not provide them. Use this event to invite guests to leave a review, route feedback to the right team, or sync ratings into your own tools.

A waitlist entry is a guest’s request to be told if a table frees up on a given service date. It is not a reservation and holds no table.

The data.object of every waitlist event is a full waitlist entry, in the same shape the API returns.

The reservation an open offer holds sits in the status awaiting_guest: a real table, held for one guest until they answer. Its sibling awaiting_guarantee holds a table while a card is being saved.

The two are not equally visible to you. A card hold on a booking you already know about stays in your feed, and is what reservation.guarantee_requested carries. An awaiting_guest hold has no published booking behind it, so it is filtered out of every reservation entrypoint: the list, the updated_since feed, a fetch by id, and the events feed. ?status=awaiting_guest is accepted and returns nothing. A hold the guest declined, or that ran out of time, becomes cancelled and stays hidden under that status too.

Read awaiting_guest as a status you will never be sent. It appears in the reference enum because that enum lists every status a reservation can take internally. It reaches you with a booking attached in one place only: created_status on the reservation.created entry in the events feed, once the guest has accepted.

reservation is null until the entry becomes a booking you can retrieve. An entry that is mid-offer has a reservation internally, but that row is not yet a published booking — so the field stays null rather than handing you an id that would 404. Once it is set, GET /v1/reservations/{id} resolves it.

When an entry becomes a booking you receive both waitlist_entry.promoted and the ordinary reservation.created, whose source is waitlist.

That is deliberate: a consumer that only handles reservation.* stays complete and sees the booking appear like any other, while a consumer that tracks the queue learns which request it came from. If you handle both, deduplicate on the reservation’s own id — the two events describe one booking.

A guest joined the waitlist. status is active, and reservation is null — nothing is held.

{
"id": "evt_D4eF5gH6iJ7kL8mN9oP0qR1s",
"object": "event",
"type": "waitlist_entry.created",
"created": "2026-07-11T09:12:04Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "waitlist_entry",
"id": "wl_Os1uJlHmul2SpNy_0Ht1V6kY",
"status": "active",
"joined_via": "widget",
"service_date": "2026-07-18",
"party_size": 2,
"window_start": "19:00",
"window_end": "21:00",
"window_start_day_offset": 0,
"window_end_day_offset": 0,
"shift_name": "Dinner",
"shift_start_time": "19:00",
"shift_end_time": "22:30",
"notes": null,
"reservation": null,
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-11T11:12:04+02:00",
"guest": "gst_z7p7NMpeGZcmL52fP-erOktL"
}
}
}

A table opened up and the entry became a reservation. status is promoted and reservation carries the booking’s id.

{
"id": "evt_E5fG6hI7jK8lM9nO0pQ1rS2t",
"object": "event",
"type": "waitlist_entry.promoted",
"created": "2026-07-16T16:40:22Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "waitlist_entry",
"id": "wl_Os1uJlHmul2SpNy_0Ht1V6kY",
"status": "promoted",
"joined_via": "widget",
"service_date": "2026-07-18",
"party_size": 2,
"window_start": "19:00",
"window_end": "21:00",
"window_start_day_offset": 0,
"window_end_day_offset": 0,
"shift_name": "Dinner",
"shift_start_time": "19:00",
"shift_end_time": "22:30",
"notes": null,
"reservation": "resv_1M__exJ2ZQ7jKENZxz28djsH",
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-16T18:40:22+02:00",
"guest": "gst_z7p7NMpeGZcmL52fP-erOktL"
}
}
}

The guest changed their own request — the arrival window, the party size, or both. The entry keeps its id and its place; only what it is asking for changes.

This is one call, not a withdraw followed by a rejoin: the guest never loses their place, and there is no window in which the entry does not exist. So you will not see a cancelled / created pair for an edit, and an entry’s id stays stable across one.

{
"id": "evt_Q6rS7tU8vW9xY0zA1bC2dE3f",
"object": "event",
"type": "waitlist_entry.updated",
"created": "2026-07-12T10:04:17Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "waitlist_entry",
"id": "wl_Os1uJlHmul2SpNy_0Ht1V6kY",
"status": "active",
"joined_via": "widget",
"service_date": "2026-07-18",
"party_size": 4,
"window_start": "19:30",
"window_end": "22:00",
"window_start_day_offset": 0,
"window_end_day_offset": 0,
"shift_name": "Dinner",
"shift_start_time": "19:00",
"shift_end_time": "22:30",
"notes": null,
"reservation": null,
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-12T12:04:17+02:00"
}
}
}

The entry was withdrawn — by the guest from their own link, or by the restaurant. status is cancelled.

{
"id": "evt_F6gH7iJ8kL9mN0oP1qR2sT3u",
"object": "event",
"type": "waitlist_entry.cancelled",
"created": "2026-07-14T08:03:41Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "waitlist_entry",
"id": "wl_Os1uJlHmul2SpNy_0Ht1V6kY",
"status": "cancelled",
"joined_via": "widget",
"service_date": "2026-07-18",
"party_size": 2,
"window_start": "19:00",
"window_end": "21:00",
"window_start_day_offset": 0,
"window_end_day_offset": 0,
"shift_name": "Dinner",
"shift_start_time": "19:00",
"shift_end_time": "22:30",
"notes": null,
"reservation": null,
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-14T10:03:41+02:00",
"guest": "gst_z7p7NMpeGZcmL52fP-erOktL"
}
}
}

The entry ran out of time — the service passed without a table freeing up. status is expired.

{
"id": "evt_G7hI8jK9lM0nO1pQ2rS3tU4v",
"object": "event",
"type": "waitlist_entry.expired",
"created": "2026-07-18T21:30:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "waitlist_entry",
"id": "wl_Os1uJlHmul2SpNy_0Ht1V6kY",
"status": "expired",
"joined_via": "widget",
"service_date": "2026-07-18",
"party_size": 2,
"window_start": "19:00",
"window_end": "21:00",
"window_start_day_offset": 0,
"window_end_day_offset": 0,
"shift_name": "Dinner",
"shift_start_time": "19:00",
"shift_end_time": "22:30",
"notes": null,
"reservation": null,
"created_at": "2026-07-11T11:12:04+02:00",
"updated_at": "2026-07-18T23:30:00+02:00",
"guest": "gst_z7p7NMpeGZcmL52fP-erOktL"
}
}
}

A hold is a claim on a slot. It occupies a real table without being a booking: it carries no guest, it does not appear in GET /v1/reservations, and the table is off sale for as long as the hold lives. Holds are created with POST /v1/holds and handed back with DELETE /v1/holds/{id}, or released by the restaurant from its back office — see the API reference.

The data.object of every hold event is a hold, not a reservation. It says "object": "hold" and carries its own, smaller set of fields. Branch on data.object.object if one handler receives both.

A hold’s id shares its suffix with the reservation it becomes, so hold_AbC converts into resv_AbC.

starts_at and ends_at are the seating the hold covers. expires_at is when the hold lapses. Occupancy needs both: the table is unavailable for the seating window, but only until the hold expires.

expires_at is the server’s answer, not yours. A create may ask for a deadline and the server clamps it — a basket hold to at most 30 minutes (10 when none is asked for), a commitment hold to at most 48 hours, both to at least 5 minutes — so read it back off hold.created or the create response rather than assuming the number you sent. It is null on every terminal hold, because a hold that is over has no deadline left to honour.

When a hold becomes a booking you receive both hold.converted and reservation.created — same party, same moment. Treat hold.converted as the hold being released and count the covers off reservation.created alone, or the party is counted twice.

Creating a hold emits no reservation.created at all, so a held table stays out of the reservation feed until somebody books it.

A slot was held. status is held, reservation is null, and expires_at carries the deadline the server set.

{
"id": "evt_P3qR4sT5uV6wX7yZ8aB9cD0e",
"object": "event",
"type": "hold.created",
"created": "2026-07-04T17:12:40Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "hold",
"id": "hold_9pLr4Xm2Vd0",
"kind": "basket",
"status": "held",
"party_size": 4,
"service_date": "2026-07-11",
"starts_at": "2026-07-11T20:00:00+02:00",
"ends_at": "2026-07-11T22:00:00+02:00",
"expires_at": "2026-07-04T19:22:40+02:00",
"ended_at": null,
"reservation": null,
"tables": [
{
"object": "table",
"id": "tbl_QpZ2",
"name": "12",
"min_capacity": 2,
"max_capacity": 6,
"section": {
"object": "section",
"id": "sec_Lm8",
"name": "Main room",
"area_type": "indoor"
}
}
],
"metadata": { "your_reference": "basket-8841" },
"created_at": "2026-07-04T19:12:40+02:00",
"updated_at": "2026-07-04T19:12:40+02:00"
}
}
}

metadata is echoed back verbatim and is carried onto the reservation when the hold converts. It is readable by every API key the restaurant has issued, not only by the one that wrote it — put reconciliation ids there, not commercial terms.

The hold became the reservation named in reservation. status is converted. Both expires_at and ended_at are null: a converted hold did not end, it turned into a booking. kind still reports what the hold was.

{
"id": "evt_Q4rS5tU6vW7xY8zA9bC0dE1f",
"object": "event",
"type": "hold.converted",
"created": "2026-07-04T17:16:05Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "hold",
"id": "hold_9pLr4Xm2Vd0",
"kind": "basket",
"status": "converted",
"party_size": 4,
"service_date": "2026-07-11",
"starts_at": "2026-07-11T20:00:00+02:00",
"ends_at": "2026-07-11T22:00:00+02:00",
"expires_at": null,
"ended_at": null,
"reservation": "resv_9pLr4Xm2Vd0",
"tables": [
{
"object": "table",
"id": "tbl_QpZ2",
"name": "12",
"min_capacity": 2,
"max_capacity": 6,
"section": {
"object": "section",
"id": "sec_Lm8",
"name": "Main room",
"area_type": "indoor"
}
}
],
"metadata": { "your_reference": "basket-8841" },
"created_at": "2026-07-04T19:12:40+02:00",
"updated_at": "2026-07-04T19:16:05+02:00"
}
}
}

The hold was handed back before its deadline, and its table is on sale again. status is released and ended_at is the moment it was given back.

Two doors lead here: your own DELETE /v1/holds/{id}, and the restaurant releasing the hold from its back office — typically to seat a walk-in at that table. The payload is the same either way. If you receive a hold.released you did not ask for, the restaurant took the table back: the hold can no longer be converted (POST /v1/reservations { hold_id } answers 404), so tell the guest the time is no longer held and book fresh if they still want it.

Releasing an already-released or already-lapsed hold writes nothing and fires nothing, so you receive this event once per hold.

{
"id": "evt_R5sT6uV7wX8yZ9aB0cD1eF2g",
"object": "event",
"type": "hold.released",
"created": "2026-07-04T17:19:31Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "hold",
"id": "hold_9pLr4Xm2Vd0",
"kind": "basket",
"status": "released",
"party_size": 4,
"service_date": "2026-07-11",
"starts_at": "2026-07-11T20:00:00+02:00",
"ends_at": "2026-07-11T22:00:00+02:00",
"expires_at": null,
"ended_at": "2026-07-04T19:19:31+02:00",
"reservation": null,
"tables": [
{
"object": "table",
"id": "tbl_QpZ2",
"name": "12",
"min_capacity": 2,
"max_capacity": 6,
"section": {
"object": "section",
"id": "sec_Lm8",
"name": "Main room",
"area_type": "indoor"
}
}
],
"metadata": { "your_reference": "basket-8841" },
"created_at": "2026-07-04T19:12:40+02:00",
"updated_at": "2026-07-04T19:19:31+02:00"
}
}
}

The hold ran out of time and its table is on sale again. status is expired and ended_at is the moment the deadline was finalised.

{
"id": "evt_S6tU7vW8xY9zA0bC1dE2fG3h",
"object": "event",
"type": "hold.expired",
"created": "2026-07-04T17:25:00Z",
"livemode": true,
"api_version": "2026-06-27",
"data": {
"object": {
"object": "hold",
"id": "hold_4kNt7Yq1Wb3",
"kind": "commitment",
"status": "expired",
"party_size": 2,
"service_date": "2026-07-06",
"starts_at": "2026-07-06T12:30:00+02:00",
"ends_at": "2026-07-06T14:00:00+02:00",
"expires_at": null,
"ended_at": "2026-07-04T19:25:00+02:00",
"reservation": null,
"tables": [
{
"object": "table",
"id": "tbl_Rk71",
"name": "3",
"min_capacity": 2,
"max_capacity": 4,
"section": {
"object": "section",
"id": "sec_Lm8",
"name": "Main room",
"area_type": "indoor"
}
}
],
"metadata": {},
"created_at": "2026-07-02T19:20:11+02:00",
"updated_at": "2026-07-04T19:25:00+02:00"
}
}
}