Bokko is getting ready to launch. This site previews the features available at launch. Registration is currently closed. Notify me
Docs
Ctrl+K

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.

Tip
Configuring and using webhooks requires an eligible Bokko subscription that includes Public API access — contact Bokko support about availability.
  • 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#

Tip
You can also manage webhook configuration in the dashboard (set URL, rotate secret, activate/deactivate).

You can register the webhook URL at the PUT /v1/webhooks/config endpoint. This requires the webhook.manage capability.

bash
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:

RangeExamples
Loopbacklocalhost, 127.0.0.1, ::1, 0.0.0.0
Private network10.x.x.x, 172.16-31.x.x, 192.168.x.x
Link-local169.254.x.x
Tip
Upon the first 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ényLeírás
booking.requestedNew appointment request received (always the request state, sequence 1 — an automatically accepted request is followed by booking.confirmed)
booking.confirmedBooking confirmed — by the business (acceptanceMode manual) or by the service's automatic acceptance rule (acceptanceMode automatic)
booking.declinedBooking declined
booking.cancelledBooking cancelled
booking.reschedule_proposedAppointment modification proposed
booking.reschedule_confirmed>-
booking.completedBooking completed
booking.no_showGuest did not show up
EventDescription
booking.requestedNew appointment request received (always the request state)
booking.confirmedBooking accepted — by the business or by the service’s automatic acceptance rule
booking.declinedBooking declined
booking.cancelledBooking cancelled
booking.reschedule_proposedAppointment modification proposed
booking.reschedule_confirmedModified appointment confirmed
booking.completedBooking completed
booking.no_showGuest did not show up

Payload Format#

Every webhook delivery is a JSON object with the following structure:

Info
deliveryId identifies the business event (event × booking × transition), not the attempt: a deterministic 32-character hex string. Every attempt and every re-delivery of the same event carries the same value — use it as your deduplication key.
json
{
  "event": "booking.confirmed",
  "deliveryId": "3f2a9c0d8b7e4f1a2c3d4e5f6a7b8c9d",
  "timestamp": "2026-04-01T09:00:00.000Z",
  "salonSlug": "precision-cuts",
  "transition": {
    "id": "tr_8Kq2mZ4pX7",
    "sequence": 2,
    "occurredAt": "2026-04-01T09:15:00Z",
    "from": "requested",
    "to": "confirmed"
  },
  "booking": {
    "bookingId": "abc123",
    "publicReference": "BK-A2B3C4D5",
    "status": "confirmed",
    "locationId": "loc_downtown_01",
    "locationSnapshot": {
      "id": "loc_downtown_01",
      "locationSlug": "downtown",
      "publicLocationName": "Bokko Downtown",
      "address": {
        "city": "Budapest",
        "street": "Deak Ferenc utca 1.",
        "zip": "1052"
      },
      "timezone": "Europe/Budapest"
    },
    "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",
    "stateVersion": 2,
    "acceptanceMode": "manual"
  }
}

The confirmedSlot contains the finalized appointment time for active confirmed-style lifecycle states such as confirmed, completed, and noShow. For cancelled or declined bookings, confirmedSlot is always null in the API, even if the booking had been confirmed earlier. The status field determines whether the time slot is currently occupied.

Ordering, Identity and Deduplication#

Three separate keys — do not substitute one for another:

ConcernKeyRule
State orderingtransition.sequence (= booking.stateVersion)Delivery order is not guaranteed. Per booking, apply an event only if its sequence is greater than the last one you applied; otherwise it is stale.
Business-mutation identitytransition.idA new immutable id for every state change (status transition or change of the confirmed time); null for a creation. Not sortable.
Delivery deduplicationdeliveryIdIdentical on every attempt and re-delivery of the same event.

timestamp and transition.occurredAt are informative only — two events of one transaction can share the same time. Every booking webhook represents a state change, so every event carries a sequence. Two successive time changes of a confirmed booking produce two booking.reschedule_confirmed events with sequence N and N+1 and two different deliveryIds.

Automatic acceptance. When a service's acceptance rule accepts a request in the same call, two events are sent: booking.requested (sequence: 1, status: requested, empty confirmedSlot) and booking.confirmed (sequence: 2, acceptanceMode: automatic). Each payload describes the state of ITS event, not the current state of the booking.

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 EnumWebhook Event
requestedbooking.requested (booking created)
confirmedbooking.confirmed, booking.reschedule_confirmed (if confirmed after rescheduling)
declinedbooking.declined
cancelledbooking.cancelled
rescheduleProposedbooking.reschedule_proposed
completedbooking.completed
noShowbooking.no_show
Info
The name of the 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:

HeaderDescription
X-Bokko-Signaturesha256=<hex> — HMAC-SHA256 signature over the body
X-Bokko-Delivery-IdBusiness event ID — deduplication key, identical on every attempt
X-Bokko-EventName of the event (e.g., booking.confirmed)
X-Bokko-TimestampISO 8601 timestamp of the first dispatch, identical on every attempt
X-Bokko-AttemptAttempt number (1–3)

Signature Verification#

Warning
Always verify the signature before processing the payload! Use a timing-safe comparison to avoid timing attacks.

Node.js#

javascript
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#

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#

RuleValue
Number of attemptsat most 3 per event
Timeout8 seconds per attempt
Follow redirectsNo
Delivery guaranteeAt-least-once — the same event can arrive more than once
Retried responsestimeout, network error, 408, 429, 5xx
Not retriedevery other status, including 3xx
Schedule2nd attempt ~15 s – 2 minutes after the 1st, 3rd attempt ~2–6 minutes after the 2nd
Retry-Afterhonoured on 429 / 503 (seconds or HTTP date, max 600 s); it only extends the wait

Every attempt sends the same body bytes with the same deliveryId and timestamp. The signature is computed on each attempt with the current secret, and retries go to the currently configured URL — after a secret rotation, a retried delivery is signed with the new secret.

Possible outcomes:

  • success — 2xx response code received
  • timeout — endpoint did not respond within 8 seconds
  • network_error — endpoint was not reachable
  • http_error — non-2xx response code (e.g., 500)
Tip
A delivery that ends exhausted (retryable failure on the last attempt) or failed (non-retryable response, or the webhook configuration was deactivated) is not attempted again. Monitor these in the delivery log and reconcile with the GET /v1/bookings endpoint when needed.

Delivery Log#

The GET /v1/webhooks/deliveries endpoint lists one record per business event, with every attempt in attempts and the delivery state in status (processing, retry_scheduled, succeeded, failed, exhausted). Records are kept for 12 months; a single query can cover at most 90 days.

Filtering Options:

ParameterDescription
eventEvent type filter (e.g., booking.confirmed)
outcomeOutcome of the most recent attempt (success, timeout, network_error, http_error)
from / toDate 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:

bash
curl -X POST https://api.bokko.io/v1/webhooks/config/rotate-secret \
  -H "Authorization: Bearer {API_KEY}"
Warning
Rotation immediately invalidates the old secret. The new secret is returned once in the response — save it immediately. After rotation, every attempt — including retries of events created earlier — is signed with the new secret.

Best Practices#

  1. Order by transition.sequence, deduplicate by deliveryId — see Ordering, Identity and Deduplication.
  2. Process idempotently, keyed by deliveryId — delivery is at-least-once, so the same event can arrive again (for example when your 200 response was lost after a timeout). Store processed deliveryId values and acknowledge repeats with 200 without re-processing.
  3. Respond quickly with 200, perform actual processing asynchronously — the 8-second timeout can be tight for complex logic.
  4. Periodically reconcile with the GET /v1/bookings endpoint to ensure you don't miss any events.
  5. For local development, use webhook.site or ngrok to receive webhooks.