Booking Flow
End-to-end integration of the booking process — service discovery, availability, booking creation, lifecycle monitoring, cancellation.
Overview#
The Bokko booking process consists of 5 phases:
- Discover — Query available services and staff members.
- Plan — Search for available time slots for a given date range.
- Reserve — Create a booking.
- Lifecycle — Monitor booking status (webhook or polling).
- Cancel — Cancel a booking if necessary.
Discover — Services and Staff#
List Services (GET /services)#
curl -X GET "https://api.bokko.io/v1/services?salonSlug=example-salon" \
-H "Authorization: Bearer bk_live_..."Returns all active, online bookable services. You will need the serviceId for the availability search and when creating a booking.
List Staff Members (GET /staff)#
curl -X GET "https://api.bokko.io/v1/staff?salonSlug=example-salon" \
-H "Authorization: Bearer bk_live_..."In the availability search response, slots[].staffId indicates which staff member performs the service — you can display this based on the GET /staff response.
Plan — Search for Available Slots (POST /availability/search)#
Request Body Fields#
| Field | Type | Description |
|---|---|---|
salonSlug | string (mandatory) | Identifier for the booked salon |
serviceId | string (mandatory) | Identifier for the requested service |
staffId | string (optional) | If provided, only slots for that staff member appear. |
dateRange.from | YYYY-MM-DD (mandatory) | Start of the search period (salon's local timezone) |
dateRange.to | YYYY-MM-DD (mandatory) | End of search period — inclusive, max 31 days after from |
<!-- doc-example: request POST /availability/search -->
curl -X POST "https://api.bokko.io/v1/availability/search" \
-H "Authorization: Bearer bk_live_..." \
-H "Content-Type: application/json" \
-d '{
"salonSlug": "example-salon",
"serviceId": "svc_haircut_01",
"staffId": "staff_anna_01",
"dateRange": {
"from": "2026-05-12",
"to": "2026-05-18"
}
}'Response Fields and Timezone Semantics#
Request/propose phase — Salon's local timezone:
slots[].date—YYYY-MM-DDin the salon's local timezone (not UTC).slots[].startTime—HH:MMsalon-local wall-clock time.slots[].endTime—HH:MMsalon-local wall-clock time.slots[].serviceId,slots[].staffId— Identifiers.
UTC timestamp:
snapshotAt— UTC ISO 8601 timestamp (moment of snapshot issuance).
Confirmed lifecycle — UTC ISO 8601: Once a booking is confirmed, the timestamps (confirmedAt, cancelledAt, completedAt) arrive in UTC ISO 8601 format — never interpret these as salon local time.
Note on confirmedSlot: For bookings in cancelled or declined status, the confirmedSlot field is always null in the API, even if the booking was previously confirmed. This indicates that the time slot is no longer reserved.
Truncation and Suppression#
If the search would return more than 500 slots, the response is truncated:
meta.truncated: trueindicates truncation.meta.returnedSlots— number of slots actually returned.meta.slotLimit— the truncation limit (500).
If online booking is disabled in the salon:
meta.availabilitySuppressed: truedata.slotsis an empty array.
Errors#
availability.date_range_too_wide(400) — ThedateRangeis wider than 31 days.
Reserve — Creating a Booking (POST /bookings)#
Idempotency-Key#
Every booking creation request requires an Idempotency-Key header in RFC 4122 UUID v1–v5 format. The idempotency window is 24 hours.
Generate the UUID once into a shell variable. For retries, send exactly the same key:
<!-- doc-example: request POST /bookings -->
IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST "https://api.bokko.io/v1/bookings" \
-H "Authorization: Bearer bk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
-d '{
"salonSlug": "example-salon",
"serviceId": "svc_haircut_01",
"expectedPricingVersion": "a1b2c3d4e5f6",
"staffId": "staff_anna_01",
"slot": {
"date": "2026-05-12",
"startTime": "10:00"
},
"guest": {
"name": "Test Guest",
"phone": "+36301234567"
}
}'Booking Statuses (State Machine)#
| Status | Description |
|---|---|
requested | Created, waiting for confirmation |
confirmed | Confirmed by the salon |
declined | Declined |
rescheduleProposed | Rescheduling proposal received |
cancelled | Cancelled |
completed | Completed |
noShow | Guest did not show up |
Lifecycle Monitoring#
Webhook-First Approach (Recommended)#
Set up a webhook at the PUT /webhooks/config endpoint — you will receive notifications for every booking change.
The 8 webhook events:
booking.requestedbooking.confirmedbooking.declinedbooking.cancelledbooking.reschedule_proposedbooking.reschedule_confirmedbooking.completedbooking.no_show
Every event arrives with an X-Bokko-Signature: sha256=<hex> header with a HMAC-SHA256 signature — verify it to establish authenticity. Detailed guide: Webhooks →
Polling Fallback#
If webhooks are not available, you can query the booking status:
curl -X GET "https://api.bokko.io/v1/bookings/{bookingId}?salonSlug=example-salon" \
-H "Authorization: Bearer bk_live_..."Cancel — Cancelling a Booking (POST /bookings/{bookingId}/cancel)#
IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST "https://api.bokko.io/v1/bookings/{bookingId}/cancel?salonSlug=example-salon" \
-H "Authorization: Bearer bk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${IDEMPOTENCY_KEY}"Cancellation is idempotent: if the booking is already in cancelled status, the request returns with a 409 booking.invalid_transition error — this is not a real error, the cancellation has successfully occurred.
Errors During the Flow#
| Code | HTTP | When it occurs |
|---|---|---|
booking.slot_unavailable | 409 | The requested time slot is no longer available. |
booking.service_pricing_changed | 409 | Pricing changed between availability search and booking. |
idempotency.payload_mismatch | 409 | Same Idempotency-Key, different payload. |
idempotency.request_in_progress | 503 | Request processing is still in progress (retryable). |
auth.plan_insufficient | 403 | The key's tenant does not have a Pro subscription. |
Retry Strategy#
idempotency.request_in_progress(503) — retryable: yes, with the same Idempotency-Key.booking.slot_unavailable(409) — retryable: no; search for a new time slot.booking.service_pricing_changed(409) — retryable: no; query the service list again.
Best Practices#
- Generate Idempotency-Key into a shell variable, not inline — it should not change during retries.
- Webhook priority: Use polling only as a fallback; use webhooks for real-time notifications.
- Timezone: Availability slot
date/startTime/endTimeare salon-local;confirmedAtand similar lifecycle fields are UTC — do not mix them up. - Slot freshness: Availability snapshots can become outdated quickly; do not store them in cache for longer than 30 seconds before booking.