- Overview
- Discover — Services and Staff
- List Services (GET /services)
- List Staff Members (GET /staff)
- Plan — Search for Available Slots (POST /availability/search)
- Request Body Fields
- Response Fields and Timezone Semantics
- Truncation and Suppression
- Errors
- Reserve — Creating a Booking (POST /bookings)
- Idempotency-Key
- Request acceptance: manual or automatic
- Deposit contract (breaking)
- Booking Statuses (State Machine)
- Lifecycle Monitoring
- Webhook-First Approach (Recommended)
- Listing Bookings (GET /bookings)
- Polling Fallback
- Cancel — Cancelling a Booking (POST /bookings/{bookingId}/cancel)
- Errors During the Flow
- Retry Strategy
- Best Practices
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-business&locationSlug=downtown" \
-H "Authorization: Bearer bk_live_..."Returns all active, online bookable services at the given location. 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-business&locationSlug=downtown" \
-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 service provider business |
locationSlug | string (mandatory) | Identifier for the location |
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 (business's local timezone) |
dateRange.to | YYYY-MM-DD (mandatory) | End of search period — inclusive, max 31 days after from |
curl -X POST "https://api.bokko.io/v1/availability/search" \
-H "Authorization: Bearer bk_live_..." \
-H "Content-Type: application/json" \
-d '{
"salonSlug": "example-business",
"locationSlug": "downtown",
"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 — Business local timezone:
slots[].date—YYYY-MM-DDin the business's local timezone (not UTC).slots[].startTime—HH:MMbusiness-local wall-clock time.slots[].endTime—HH:MMbusiness-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: Booking metadata (createdAt, updatedAt) arrive in UTC ISO 8601 format.
Confirmed lifecycle — Business local timezone: The confirmed appointment (confirmedSlot.date, startTime, endTime) also arrive in the business's local timezone, matching the availability search format. This eliminates the need for complex timezone conversion for display.
Note on confirmedSlot: The confirmedSlot field is populated 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.
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 for the business:
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:
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-business",
"locationSlug": "downtown",
"serviceId": "svc_haircut_01",
"expectedPricingVersion": "a1b2c3d4e5f6",
"staffId": "staff_anna_01",
"slot": {
"date": "2026-05-12",
"startTime": "10:00"
},
"guest": {
"name": "Test Guest",
"phone": "+36301234567"
},
"consent": {
"privacyNoticeVersion": "v1-open-beta-r9",
"termsVersion": "v1-open-beta-r9",
"locale": "hu",
"acceptedAt": "2026-05-12T09:41:00.000Z",
"policyConsentGiven": true
}
}'Request acceptance: manual or automatic#
A booking created through the API is always a request. What happens next depends on the service's requestAcceptancePolicy (from GET /services):
manual— the booking is returned asrequested; the business accepts, declines or proposes
another time.
automatic— the business has set an acceptance rule for this service in advance:- if automatic acceptance can be carried out, the business's acceptance is recorded in the same
call: the response is status: confirmed, acceptanceMode: automatic, stateVersion: 2, and two webhooks follow (booking.requested, then booking.confirmed);
- if automatic acceptance cannot be carried out but the request can still be validly forwarded,
the response is status: requested and the business decides;
- if the final check proves the slot unavailable, no request is created:
409 booking.slot_no_longer_available — search for another time.
Deposit contract (breaking)#
When GET /services returns deposit.required: true for a service, POST /bookings requires:
depositTermsAcceptance: { termsVersion, acceptedAt }—termsVersionexactly as listed; your
attestation that the guest accepted these deposit terms;
guest.email— the payment link and its deadline are sent there.
Missing or stale terms return 422 booking.deposit_terms_required / booking.deposit_terms_changed. When such a request is accepted automatically, the response carries data.nextAction.depositPaymentUrl (only on the live response — never in a webhook or an idempotent replay; treat it as a secret bearer link).
Booking Statuses (State Machine)#
| Status | Description |
|---|---|
requested | Request created, waiting for the business's decision |
confirmed | Accepted — by the business, or by the service's automatic acceptance rule (acceptanceMode) |
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. Delivery order is not guaranteed: order the events of one booking by transition.sequence (= booking.stateVersion) and deduplicate by deliveryId. Detailed guide: Webhooks →
Listing Bookings (GET /bookings)#
If you want to synchronize the status of multiple bookings at once:
curl -X GET "https://api.bokko.io/v1/bookings?salonSlug=example-business&locationSlug=downtown&status=confirmed&from=2026-05-12&to=2026-05-18" \
-H "Authorization: Bearer bk_live_..."Filtering parameters:
status(optional): Only bookings with the given status.locationSlug(optional): Filter by location.fromandto(optional, in pairs): Date range filter based on the booking date.
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-business" \
-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-business" \
-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. |
booking.slot_no_longer_available | 409 | Automatic acceptance: the final check proved the slot unavailable; no request was created. |
booking.deposit_terms_required | 422 | The service requires a deposit and depositTermsAcceptance is missing. |
booking.deposit_terms_changed | 422 | The accepted deposit terms version is stale. |
booking.deposit_unavailable | 409 | Deposit collection is temporarily unavailable. |
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 service provider does not have a subscription that includes Public API access. |
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.booking.slot_no_longer_available(409) — retryable: no; search for a new time slot.booking.deposit_terms_changed(422) — retryable: no; query the service list again and re-confirm the deposit terms with the guest.
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 business-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.