Integration
Webhooks
Signed notifications to your server whenever an order or a reservation changes, checking the signature, retries and testing.
A webhook tells your server when an order or a reservation changes, so your server does not have to keep asking. This guide covers setting one up, the events that arrive, checking their signature and the rules of delivery.
Setting up a webhook#
A webhook belongs to an API key. The venue's owner enters its address in the panel, on the key:
- On the API Keys page, select Edit beside the key. You can also enter the address while creating a new key.
- In Webhook address, enter the address on your server that will receive the events: a public
https://address. - When you save, the signing secret (
whsec_…) is shown once. Copy it into your server's configuration.
- New Secret creates a new secret, and the old one stops working at once. Put the new secret on your server straight away, or the signatures of incoming notifications cannot be checked.
- Clearing the address removes the webhook and its signing secret together. No event is sent for a key with no address.
An order's event usually arrives within seconds. When Cibusy is idle it can take up to about a minute, because a sweep that runs once a minute is what delivers it then. A reservation's event is always delivered by that sweep, within about a minute: a reservation does not change by the second, but when somebody decides something. Do not build anything that relies on an event being instant.
Which orders are followed#
A key's webhook follows the orders that key placed, or added a round to.
- The first
order.updatedfor an order is sent when the order is placed, with the order as it started. - An order is followed for 48 hours after it was placed. A change after that is not announced;
GET /orders/{orderId}keeps telling you its state. - An address added to a key later starts receiving the updates of that key's open orders from the last 48 hours.
- No events are sent for the orders of a revoked key.
Any change to the order as the API shows it is an event: its status, payment status, table, total, the amount paid, when it was closed, or its lines being added, or their quantity or status changing. A name or a note corrected on the venue's side is not one.
Which reservations are followed#
A key's webhook follows the reservations that key took as well; the Can take reservations permission is all the key needs for it.
- The first
reservation.updatedfor a reservation is sent after it is taken, with the reservation as it was made. - Later events come when the venue accepts or declines the request; when the reservation is called off by the guest, by the venue or for want of an answer; when the guest is checked in at the door; and when the visit ends or nobody came.
- A reservation is followed from the day it is made until 48 hours after the time it was for; following ends once it is declined, called off or over and that last state was delivered. After that,
GET /reservations/{reservationId}keeps telling you its state. - An address added to a key later starts receiving the updates of the reservations that key still has open.
- No events are sent for the reservations of a revoked key.
The changes that make an event are the reservation's status, its time, its number of guests and who called it off. A note the venue corrects is not one.
Events#
type | When it comes |
|---|---|
order.updated | An order the key placed, or added a round to, has changed. data is the same order GET /orders/{orderId} returns. |
reservation.updated | A reservation the key took has changed. data is the same reservation GET /reservations/{reservationId} returns. |
webhook.test | You asked for it with POST /webhooks/test. data says whose test it is; no order or reservation has changed. |
Each event is a JSON body written by the same rules as the rest of the API (camelCase names, enum names, UTC times), sent compactly in UTF-8 without a byte-order mark. It is shown indented here, to be read:
{
"id": "evt_0f8e2c1a7d444b0e9d2a5c3e7b1f9a60",
"type": "order.updated",
"createdAt": "2026-10-01T09:42:17.481Z",
"data": { }
}idisevt_followed by 32 lowercase hexadecimal characters. Theidof anorder.updatedorreservation.updatedevent stays the same every time the same change to the same order or reservation is delivered again, and differs for every new change: it is what to de-duplicate on. Awebhook.testevent has a newidevery time.createdAtis when Cibusy prepared that delivery.
The fields of order.updated#
idstringThe event's id: evt_ and 32 lowercase hexadecimal characters. An order.updated or reservation.updated event carries the same id every time the same change to the same order or reservation is delivered again (after a failed attempt, say); de-duplicate on it. Every new change has an id of its own, even one that takes an order back to a state it was in before. A webhook.test event has a new id every time. Also sent in the X-Cibusy-Event-Id header.
typestringThe kind of event: order.updated, reservation.updated or webhook.test. Also sent in the X-Cibusy-Event-Type header.
Values:order.updated
createdAtstring · date-timeWhen this delivery was prepared, in UTC. A retry is prepared again, so it can differ between attempts. Events about one order or reservation can arrive out of order; this is how to tell which is newer.
dataobjectThe order as it was when this delivery was prepared, in the form GET /orders/{orderId} returns it.
Fields of Order
idstring · uuidThe order's id. Use it with GET /orders/{orderId}.
numberintegerThe number the venue calls the order by, counted from 1 within the venue. Show it to the customer; it is the one printed on the kitchen ticket. Unique within one venue only.
venueIdstring · uuidThe venue the order was placed at.
typestringHow the order reaches the diner.
DineIn- At a table: the round is added to the table's open bill, or opens one.
Takeaway- Takeaway: the customer collects it at the venue.
Delivery- Delivery: the venue brings it to the customer.
statusstringWhere the order has got to, worked out from the order's lines and its bill on every read.
Received- The venue has the order and the kitchen has not started.
Preparing- The kitchen has started on at least one line.
Ready- Every line is ready or already served.
Served- Every line on it is served.
OnTheWay- A delivery has left the venue.
Completed- The venue closed the bill.
Cancelled- The order was voided, or every line on it was struck off.
paymentStatusstringHow much of the bill has been paid at the venue.
Unpaid- Nothing has been paid at the venue.
PartiallyPaid- Part of the bill has been paid.
Paid- The bill is settled.
tableobjectnullableThe table, for a dine-in order. null for a takeaway or a delivery.
Fields of Order table
idstring · uuidThe table's id, as GET /venues/{venueId}/tables lists it.
namestringThe table's name.
customerobjectnullableWho the order is for, for a takeaway or a delivery. null for a dine-in order, and for an order nobody took the customer's details for.
Fields of Customer details
namestringnullableThe customer's name.
phonestringnullableThe customer's phone number, in international form.
addressstringnullableWhere a delivery goes.
paymentMethodstringnullableHow the customer said they would pay, if they said. null otherwise.
Cash- Cash.
Card- Card.
MealCard- Meal card.
notestringnullableThe note for the kitchen the order was placed with. null when there was none.
linesobject[]Every line on the order, including lines other people put on a table's bill and lines since cancelled. For a dine-in order that joined a bill that was already open, the lines of the whole bill are here: the addedLineIds of the placement answer say which of them that call wrote.
Fields of Order line
idstring · uuidThe line's id. Stays the same for as long as the line is on the order.
productIdstring · uuidThe product, as the menu lists it.
productNamestringThe product's name in the venue's own words.
portionIdstring · uuidnullableThe portion ordered, as the menu lists it.
portionNamestringnullableThe portion's name as it read when the order was placed.
quantityintegerHow many of it.
unitPricenumberThe price of one unit, extras included, as it was when the order was placed.
totalnumberWhat the line comes to: unitPrice × quantity, before campaigns. 0 for a cancelled line and for one the venue gave away.
statusstringWhere the line has got to in the kitchen.
Pending- Waiting in the kitchen.
Preparing- Being prepared.
Ready- Ready.
Served- Served.
Cancelled- Cancelled. The lines of a cancelled order read this way too.
extrasobject[]The extras on each unit.
Fields of Line extra
idstring · uuidThe extra, as the menu lists it.
namestringThe extra's name.
pricenumberWhat it cost per unit when the order was placed.
removedIngredientsobject[]The ingredients left out of each unit.
Fields of Removed ingredient
idstring · uuidThe ingredient, as the menu lists it.
namestringThe ingredient's name.
notestringnullableThe note for the kitchen about this line. null when there was none.
orderedAtstring · date-timeWhen the line was put on the order, in UTC.
totalsobjectWhat the order comes to, and how much of it is paid.
Fields of Totals
subtotalnumberThe lines at menu price with their extras, before campaigns. Cancelled lines are not counted.
discountnumberWhat the venue's campaigns and discounts take off.
serviceFeenumberThe venue's service fee, charged on the food after campaigns. 0 when the venue charges none.
totalnumberWhat the order comes to.
paidnumberHow much of it has been paid at the venue.
remainingnumberHow much is still owed. 0 once the bill is paid.
currencystringThe currency of every amount, an ISO 4217 code. Always TRY.
createdAtstring · date-timeWhen the order was placed, in UTC.
closedAtstring · date-timenullableWhen the venue closed the order, in UTC. null while it is open, and for a cancelled order.
The fields of reservation.updated#
idstringThe event's id: evt_ and 32 lowercase hexadecimal characters. An order.updated or reservation.updated event carries the same id every time the same change to the same order or reservation is delivered again (after a failed attempt, say); de-duplicate on it. Every new change has an id of its own, even one that takes an order back to a state it was in before. A webhook.test event has a new id every time. Also sent in the X-Cibusy-Event-Id header.
typestringThe kind of event: order.updated, reservation.updated or webhook.test. Also sent in the X-Cibusy-Event-Type header.
Values:reservation.updated
createdAtstring · date-timeWhen this delivery was prepared, in UTC. A retry is prepared again, so it can differ between attempts. Events about one order or reservation can arrive out of order; this is how to tell which is newer.
dataobjectThe reservation as it was when this delivery was prepared, in the form GET /reservations/{reservationId} returns it.
Fields of Reservation
idstring · uuidThe reservation's id. Use it with GET /reservations/{reservationId}.
venueIdstring · uuidThe venue the table is booked at.
statusstringWhere the reservation has got to, worked out on every read, so the list, a read of one reservation and a webhook event cannot say different things about the same booking.
Pending- The venue has not answered yet. A venue that confirms bookings as they are made never shows this.
Confirmed- The venue accepted it, or confirms bookings as they are made. The table is expected.
Declined- The venue declined the request.
Cancelled- It was called off. `cancelledBy` says by whom.
Seated- The guest has arrived and was checked in at the door.
Completed- The visit is over.
NoShow- The venue accepted it and nobody came.
startsAtstring · date-timeWhen the table is for, as an instant in UTC.
guestsintegerHow many people are coming.
customerobjectWho the table is for, as the venue has it.
Fields of Reservation guest
namestringnullableThe guest's name.
phonestringnullableThe guest's phone number, in international form.
notestringnullableThe note the reservation was made with. null when there was none.
codestringnullableThe code the venue checks the guest in with at the door: eight characters the guest can read out, or show as the QR on pageUrl. Whoever holds it can see and cancel the booking, so give it to the guest and to nobody else.
pageUrlstringnullableThe reservation's own page on cibusy.com, which shows the booking and, once it is confirmed, the QR the venue scans at the door. Link to it or send it to the guest. null for a venue with no public address.
canCancelbooleanWhether the reservation can still be cancelled through the API: it stands, the guest has not been checked in and its time has not come.
checkedInAtstring · date-timenullableWhen the guest was checked in at the door, in UTC. null until then.
cancelledAtstring · date-timenullableWhen the reservation was called off, in UTC. null unless status is Cancelled.
cancelledBystringnullableWho called it off. null unless status is Cancelled.
Customer- The guest: through this API, on the reservation's own page, or in the Cibusy app.
Venue- The venue.
System- Cibusy, when the venue had not answered a request by the time it was for.
cancellationReasonstringnullableWhat was given as the reason for calling it off, in the words of whoever gave it. null unless status is Cancelled.
createdAtstring · date-timeWhen the reservation was made, in UTC.
The fields of webhook.test#
idstringThe event's id: evt_ and 32 lowercase hexadecimal characters. An order.updated or reservation.updated event carries the same id every time the same change to the same order or reservation is delivered again (after a failed attempt, say); de-duplicate on it. Every new change has an id of its own, even one that takes an order back to a state it was in before. A webhook.test event has a new id every time. Also sent in the X-Cibusy-Event-Id header.
typestringThe kind of event: order.updated, reservation.updated or webhook.test. Also sent in the X-Cibusy-Event-Type header.
Values:webhook.test
createdAtstring · date-timeWhen this delivery was prepared, in UTC. A retry is prepared again, so it can differ between attempts. Events about one order or reservation can arrive out of order; this is how to tell which is newer.
dataobjectWhose test this is.
Fields of Test event data
venueIdstring · uuidThe venue the API key belongs to.
keyPrefixstringThe first characters of the API key that sent the test, as the venue's panel lists it. Not the key.
messagestringA line saying what this is.
The request's headers#
Cibusy sends a POST to your address with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Cibusy-Webhooks/1.0 |
X-Cibusy-Event-Id | The event's id. |
X-Cibusy-Event-Type | The event's type: order.updated, reservation.updated or webhook.test. |
X-Cibusy-Signature | t=<unix seconds>,v1=<signature>: the time of this attempt, and the signature in lowercase hexadecimal. Every retry has a t and a signature of its own. |
Verifying the signature#
Check every delivery before you act on it. The signature proves that the body came from Cibusy and was not changed on the way.
- Read the raw body of the request: the exact bytes received, before any JSON parser has touched them. Verify first, then parse; re-serialising parsed JSON does not give the same bytes.
- Split the
X-Cibusy-Signatureheader on,and readt(a Unix timestamp in seconds) andv1. - Reject the request if
tis more than 5 minutes from your own clock. This stops an old delivery being replayed. Every retry has atof its own, so a delivery that is retried passes. - Compute the HMAC-SHA256 of the message
t+.+ the raw body, with the whole signing secret as the key, exactly as the panel showed it, including thewhsec_prefix. Take the UTF-8 bytes of the secret and of the message, and write the result as lowercase hexadecimal. - Compare it with
v1in constant time.
<?php
$secret = getenv('CIBUSY_WEBHOOK_SECRET'); // whsec_...
$rawBody = file_get_contents('php://input'); // the exact bytes received
$header = $_SERVER['HTTP_X_CIBUSY_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? '';
$signature = $parts['v1'] ?? '';
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!ctype_digit($timestamp)
|| abs(time() - (int) $timestamp) > 300
|| !hash_equals($expected, $signature)) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true);
// De-duplicate on $event['id'], then handle the event.
http_response_code(204);Test vector#
Check your code against this: a webhook.test event as Cibusy writes it, signed with a secret made up for the purpose. Its t is 1 October 2026 09:30:00 UTC, so step 3 rejects it unless your test sets the clock to that time or leaves that one check out.
secret whsec_PVJ2W1xHqfX6u1b0Jg9eT3nR5s8dKzYaLmQwEoCtUiA
body {"id":"evt_3f2a9c4e8b1d4f6a9e0c7b5d2a1f8e34","type":"webhook.test","createdAt":"2026-10-01T09:30:00.000Z","data":{"venueId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","keyPrefix":"cbk_a1B2c3D4","message":"This is a test event from Cibusy. No order has changed."}}
header t=1790847000,v1=0d704c590d7d639a6658090e4fbb189e4e4e0e410319f007cd509ad2e67c36c9Delivery and retries#
- Answer
2xxquickly. A2xxstatus means the event is delivered. Anything else is a failure: another status, a redirect (which is not followed), a connection that takes more than 5 seconds to open, an answer that takes more than 10 seconds in all, or a connection error. Do the work after you have answered. - Retries. After a failure Cibusy tries again, up to 9 attempts for the same change, waiting 30 seconds, then 1, 2, 4, 8, 16, 32 and 64 minutes between them: about two hours in all. It then stops trying for that change and sends nothing more until the order or the reservation changes again. A reservation's events are retried on the same schedule. Every attempt is signed afresh, with a
tof its own. - De-duplicate. A retry, and rarely a delivery that was already received, brings the same
idagain. Remember theids you have handled and ignore a repeat. - Read the state, not the history. An event carries the order or the reservation as it was when that delivery was prepared. Deliveries about one order or reservation can arrive out of order: compare
createdAtwith what you hold, or, when you are unsure which is newer, read it as it is now withGET /orders/{orderId}orGET /reservations/{reservationId}. - The address. It must be
https://, with a public host name or address. Addresses that point at private, loopback, link-local or cloud-metadata destinations are refused, both when the address is saved and when an event is sent, and a redirect is never followed. - The sending IP address. Cibusy does not publish the addresses events come from. What proves a delivery is genuine is the signature, not the IP address.
Testing#
POST /webhooks/test sends a webhook.test event to the key's address, signed as a real one is, and says what your server did. The key needs either the Can send orders or the Can take reservations permission:
curl -X POST "https://api.cibusy.com/public/v1/webhooks/test" \
-H "X-Api-Key: $CIBUSY_API_KEY"{ "delivered": true, "statusCode": 204, "error": null, "durationMs": 182 }- The answer is
200whether or not your server accepted the event;deliveredsays which. statusCodeis the status your server answered with, andnullwhen it did not answer at all.errorisnullwhen the event was delivered, and otherwise says in a short sentence what went wrong.- The call waits for your server's answer for up to 10 seconds. A test event is not retried.
- A key with no webhook address is answered
409PUBLIC_API_WEBHOOK_NOT_CONFIGURED. - A test counts against the budget of about 30 requests a minute the key shares with orders and reservations.