Skip to content

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:

  1. On the API Keys page, select Edit beside the key. You can also enter the address while creating a new key.
  2. In Webhook address, enter the address on your server that will receive the events: a public https:// address.
  3. 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.updated for 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.updated for 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#

typeWhen it comes
order.updatedAn order the key placed, or added a round to, has changed. data is the same order GET /orders/{orderId} returns.
reservation.updatedA reservation the key took has changed. data is the same reservation GET /reservations/{reservationId} returns.
webhook.testYou 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:

JSON
{
  "id": "evt_0f8e2c1a7d444b0e9d2a5c3e7b1f9a60",
  "type": "order.updated",
  "createdAt": "2026-10-01T09:42:17.481Z",
  "data": { }
}
  • id is evt_ followed by 32 lowercase hexadecimal characters. The id of an order.updated or reservation.updated event 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. A webhook.test event has a new id every time.
  • createdAt is when Cibusy prepared that delivery.

The fields of order.updated#

idstring

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

typestring

The kind of event: order.updated, reservation.updated or webhook.test. Also sent in the X-Cibusy-Event-Type header.

Values:order.updated

createdAtstring · date-time

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

dataobject

The order as it was when this delivery was prepared, in the form GET /orders/{orderId} returns it.

Fields of Order
idstring · uuid

The order's id. Use it with GET /orders/{orderId}.

numberinteger

The 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 · uuid

The venue the order was placed at.

typestring

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

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

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

The table, for a dine-in order. null for a takeaway or a delivery.

Fields of Order table
idstring · uuid

The table's id, as GET /venues/{venueId}/tables lists it.

namestring

The table's name.

customerobjectnullable

Who 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
namestringnullable

The customer's name.

phonestringnullable

The customer's phone number, in international form.

addressstringnullable

Where a delivery goes.

paymentMethodstringnullable

How the customer said they would pay, if they said. null otherwise.

Cash
Cash.
Card
Card.
MealCard
Meal card.
notestringnullable

The 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 · uuid

The line's id. Stays the same for as long as the line is on the order.

productIdstring · uuid

The product, as the menu lists it.

productNamestring

The product's name in the venue's own words.

portionIdstring · uuidnullable

The portion ordered, as the menu lists it.

portionNamestringnullable

The portion's name as it read when the order was placed.

quantityinteger

How many of it.

unitPricenumber

The price of one unit, extras included, as it was when the order was placed.

totalnumber

What the line comes to: unitPrice × quantity, before campaigns. 0 for a cancelled line and for one the venue gave away.

statusstring

Where 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 · uuid

The extra, as the menu lists it.

namestring

The extra's name.

pricenumber

What it cost per unit when the order was placed.

removedIngredientsobject[]

The ingredients left out of each unit.

Fields of Removed ingredient
idstring · uuid

The ingredient, as the menu lists it.

namestring

The ingredient's name.

notestringnullable

The note for the kitchen about this line. null when there was none.

orderedAtstring · date-time

When the line was put on the order, in UTC.

totalsobject

What the order comes to, and how much of it is paid.

Fields of Totals
subtotalnumber

The lines at menu price with their extras, before campaigns. Cancelled lines are not counted.

discountnumber

What the venue's campaigns and discounts take off.

serviceFeenumber

The venue's service fee, charged on the food after campaigns. 0 when the venue charges none.

totalnumber

What the order comes to.

paidnumber

How much of it has been paid at the venue.

remainingnumber

How much is still owed. 0 once the bill is paid.

currencystring

The currency of every amount, an ISO 4217 code. Always TRY.

createdAtstring · date-time

When the order was placed, in UTC.

closedAtstring · date-timenullable

When the venue closed the order, in UTC. null while it is open, and for a cancelled order.

The fields of reservation.updated#

idstring

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

typestring

The kind of event: order.updated, reservation.updated or webhook.test. Also sent in the X-Cibusy-Event-Type header.

Values:reservation.updated

createdAtstring · date-time

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

dataobject

The reservation as it was when this delivery was prepared, in the form GET /reservations/{reservationId} returns it.

Fields of Reservation
idstring · uuid

The reservation's id. Use it with GET /reservations/{reservationId}.

venueIdstring · uuid

The venue the table is booked at.

statusstring

Where 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-time

When the table is for, as an instant in UTC.

guestsinteger

How many people are coming.

customerobject

Who the table is for, as the venue has it.

Fields of Reservation guest
namestringnullable

The guest's name.

phonestringnullable

The guest's phone number, in international form.

notestringnullable

The note the reservation was made with. null when there was none.

codestringnullable

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

pageUrlstringnullable

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

canCancelboolean

Whether 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-timenullable

When the guest was checked in at the door, in UTC. null until then.

cancelledAtstring · date-timenullable

When the reservation was called off, in UTC. null unless status is Cancelled.

cancelledBystringnullable

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

What was given as the reason for calling it off, in the words of whoever gave it. null unless status is Cancelled.

createdAtstring · date-time

When the reservation was made, in UTC.

The fields of webhook.test#

idstring

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

typestring

The kind of event: order.updated, reservation.updated or webhook.test. Also sent in the X-Cibusy-Event-Type header.

Values:webhook.test

createdAtstring · date-time

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

dataobject

Whose test this is.

Fields of Test event data
venueIdstring · uuid

The venue the API key belongs to.

keyPrefixstring

The first characters of the API key that sent the test, as the venue's panel lists it. Not the key.

messagestring

A line saying what this is.

The request's headers#

Cibusy sends a POST to your address with these headers:

HeaderValue
Content-Typeapplication/json
User-AgentCibusy-Webhooks/1.0
X-Cibusy-Event-IdThe event's id.
X-Cibusy-Event-TypeThe event's type: order.updated, reservation.updated or webhook.test.
X-Cibusy-Signaturet=<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.

  1. 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.
  2. Split the X-Cibusy-Signature header on , and read t (a Unix timestamp in seconds) and v1.
  3. Reject the request if t is more than 5 minutes from your own clock. This stops an old delivery being replayed. Every retry has a t of its own, so a delivery that is retried passes.
  4. 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 the whsec_ prefix. Take the UTF-8 bytes of the secret and of the message, and write the result as lowercase hexadecimal.
  5. Compare it with v1 in 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.

Text
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=0d704c590d7d639a6658090e4fbb189e4e4e0e410319f007cd509ad2e67c36c9

Delivery and retries#

  • Answer 2xx quickly. A 2xx status 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 t of its own.
  • De-duplicate. A retry, and rarely a delivery that was already received, brings the same id again. Remember the ids 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 createdAt with what you hold, or, when you are unsure which is newer, read it as it is now with GET /orders/{orderId} or GET /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"
JSON
{ "delivered": true, "statusCode": 204, "error": null, "durationMs": 182 }
  • The answer is 200 whether or not your server accepted the event; delivered says which.
  • statusCode is the status your server answered with, and null when it did not answer at all. error is null when 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 409 PUBLIC_API_WEBHOOK_NOT_CONFIGURED.
  • A test counts against the budget of about 30 requests a minute the key shares with orders and reservations.