Webhooks
Real-time notifications about booking events, HMAC-SHA256 signature verification.
Overview#
The Bokko webhook system sends real-time HTTP notifications when the status of a booking changes. This allows your system to respond immediately to events without having to constantly poll the API.
- Real-time: Notifications are sent at the moment a status change occurs.
- HTTPS: Webhooks can only be sent to secure endpoints.
- HMAC-SHA256 Signature: Every payload is cryptographically signed; you can verify its authenticity.
Configuration#
You can register the webhook URL at the PUT /v1/webhooks/config endpoint. This requires the webhook.manage capability.
curl -X PUT https://api.bokko.io/v1/webhooks/config \
-H "Authorization: Bearer {API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/webhooks/bokko" }'URL Requirements:
- HTTPS protocol is mandatory.
- Maximum 500 characters long.
- Must not contain authentication credentials (user:pass@).
Restricted Address Ranges:
| Range | Examples |
|---|---|
| Loopback | localhost, 127.0.0.1, ::1, 0.0.0.0 |
| Private network | 10.x.x.x, 172.16-31.x.x, 192.168.x.x |
| Link-local | 169.254.x.x |
PUT call, the system automatically generates a HMAC secret (32 bytes = 64 hex characters). This secret is returned once in the response — save it in a secure location! Subsequent PUT calls will not return it again.Events#
| Esemény | Leírás |
|---|---|
booking.requested | New booking received |
booking.confirmed | Booking confirmed |
booking.declined | Booking declined |
booking.cancelled | Booking cancelled |
booking.reschedule_proposed | Appointment modification proposed |
booking.reschedule_confirmed | Modified appointment confirmed |
booking.completed | Booking completed |
booking.no_show | Guest did not show up |
| Event | Description |
|---|---|
booking.requested | New booking received |
booking.confirmed | Booking confirmed |
booking.declined | Booking declined |
booking.cancelled | Booking cancelled |
booking.reschedule_proposed | Appointment modification proposed |
booking.reschedule_confirmed | Modified appointment confirmed |
booking.completed | Booking completed |
booking.no_show | Guest did not show up |
Payload Format#
Every webhook delivery is a JSON object with the following structure:
deliveryId varies by event type: for the booking.requested event, it is a deterministic 32-character hex string (for receiver-side deduplication), while for all other events, it is a UUID v4. Both forms can be safely used as map keys and HTTP header values.<!-- doc-example: webhook-payload -->
{
"event": "booking.confirmed",
"deliveryId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-04-01T09:00:00.000Z",
"salonSlug": "precision-cuts",
"booking": {
"bookingId": "abc123",
"publicReference": "BK-A2B3C4D5",
"status": "confirmed",
"serviceId": "svc_haircut_01",
"serviceName": "Haircut",
"staffId": "staff_anna_01",
"staffName": "Anna",
"guestName": "Peter Smith",
"requestedSlot": {
"date": "2026-04-05",
"startTime": "10:00",
"timezone": "Europe/Budapest"
},
"confirmedSlot": {
"date": "2026-04-05",
"startTime": "10:00",
"endTime": "10:45",
"timezone": "Europe/Budapest"
},
"note": null,
"createdAt": "2026-04-01T09:00:00Z",
"updatedAt": "2026-04-01T09:15:00Z"
}
}The confirmedSlot contains the finalized appointment time, if such exists for the booking (e.g. in confirmed, completed or no_show status). For cancelled or declined bookings, this value is always null in the API payload, even if the booking was previously confirmed. This reflects that the time slot is no longer occupied.
Status Enum ↔ Event Name Mapping#
The booking status field uses a camelCase enum (Bokko Public API convention), while the webhook event names use a snake_case namespaced format (REST / event sourcing convention). They differ intentionally — clients should apply the following mapping:
| BookingStatus Enum | Webhook Event |
|---|---|
requested | booking.requested (booking created) |
confirmed | booking.confirmed, booking.reschedule_confirmed (if confirmed after rescheduling) |
declined | booking.declined |
cancelled | booking.cancelled |
rescheduleProposed | booking.reschedule_proposed |
completed | booking.completed |
noShow | booking.no_show |
booking.no_show event is in snake_case (no_show), not camelCase (noShow). However, the status field of the Booking schema uses camelCase (noShow, rescheduleProposed). This discrepancy is an intentional design choice.HTTP Headers#
Every webhook request contains the following custom headers:
| Header | Description |
|---|---|
X-Bokko-Signature | sha256=<hex> — HMAC-SHA256 signature over the body |
X-Bokko-Delivery-Id | UUID — deduplication key |
X-Bokko-Event | Name of the event (e.g., booking.confirmed) |
X-Bokko-Timestamp | ISO 8601 timestamp |
Signature Verification#
Node.js#
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
const sig = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(sig, 'hex'),
Buffer.from(expected, 'hex')
);
}Python#
import hmac
import hashlib
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
sig = signature.replace('sha256=', '')
return hmac.compare_digest(sig, expected)Delivery Rules#
| Rule | Value |
|---|---|
| Number of attempts | 1 (no retries) |
| Timeout | 8 seconds |
| Follow redirects | No |
| Delivery guarantee | At-most-once |
Possible outcomes:
success— 2xx response code receivedtimeout— endpoint did not respond within 8 secondsnetwork_error— endpoint was not reachablehttp_error— non-2xx response code (e.g., 500)
GET /v1/bookings endpoint.Delivery Log#
You can review deliveries from the past 90 days at the GET /v1/webhooks/deliveries endpoint.
Filtering Options:
| Parameter | Description |
|---|---|
event | Event type filter (e.g., booking.confirmed) |
outcome | Outcome filter (success, timeout, network_error, http_error) |
from / to | Date range (ISO 8601) |
Pagination is cursor-based: provide the meta.cursor field from the response in the cursor parameter of the next request.
Secret Rotation#
If your secret is compromised, you can generate a new secret:
curl -X POST https://api.bokko.io/v1/webhooks/config/rotate-secret \
-H "Authorization: Bearer {API_KEY}"Best Practices#
- Deduplicate based on
deliveryId— although at-most-once is the guarantee, it is worth checking in case of network anomalies. - Respond quickly with 200, perform actual processing asynchronously — the 8-second timeout can be tight for complex logic.
- Periodically reconcile with the
GET /v1/bookingsendpoint to ensure you don't miss any events. - For local development, use webhook.site or ngrok to receive webhooks.