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.

Tipp
A Bokko Pro subscription is required to configure and use webhooks.
  • 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#

Tipp
You can also manage webhook configuration in the dashboard (set URL, rotate secret, activate/deactivate). Detailed UI guide: Help: Webhook configuration.

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
Tipp
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 booking received
booking.confirmedBooking confirmed
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
EventDescription
booking.requestedNew booking received
booking.confirmedBooking confirmed
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
The format of 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 -->

json
{
  "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 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-IdUUID — deduplication key
X-Bokko-EventName of the event (e.g., booking.confirmed)
X-Bokko-TimestampISO 8601 timestamp

Signature Verification#

Figyelmeztetés
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 attempts1 (no retries)
Timeout8 seconds
Follow redirectsNo
Delivery guaranteeAt-most-once

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)
Tipp
Since there are no automatic retries, it is recommended to monitor failed deliveries from the delivery log and, if necessary, synchronize with the GET /v1/bookings endpoint.

Delivery Log#

You can review deliveries from the past 90 days at the GET /v1/webhooks/deliveries endpoint.

Filtering Options:

ParameterDescription
eventEvent type filter (e.g., booking.confirmed)
outcomeOutcome filter (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}"
Figyelmeztetés
Rotation immediately invalidates the old secret. The new secret is returned once in the response — save it immediately. After rotation, all incoming webhooks will be signed with the new secret.

Best Practices#

  1. Deduplicate based on deliveryId — although at-most-once is the guarantee, it is worth checking in case of network anomalies.
  2. Respond quickly with 200, perform actual processing asynchronously — the 8-second timeout can be tight for complex logic.
  3. Periodically reconcile with the GET /v1/bookings endpoint to ensure you don't miss any events.
  4. For local development, use webhook.site or ngrok to receive webhooks.