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

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:

  1. Discover — Query available services and staff members.
  2. Plan — Search for available time slots for a given date range.
  3. Reserve — Create a booking.
  4. Lifecycle — Monitor booking status (webhook or polling).
  5. Cancel — Cancel a booking if necessary.

Discover — Services and Staff#

List Services (GET /services)#

bash
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)#

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

FieldTypeDescription
salonSlugstring (mandatory)Identifier for the service provider business
locationSlugstring (mandatory)Identifier for the location
serviceIdstring (mandatory)Identifier for the requested service
staffIdstring (optional)If provided, only slots for that staff member appear.
dateRange.fromYYYY-MM-DD (mandatory)Start of the search period (business's local timezone)
dateRange.toYYYY-MM-DD (mandatory)End of search period — inclusive, max 31 days after from
bash
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-DD in the business's local timezone (not UTC).
  • slots[].startTime — HH:MM business-local wall-clock time.
  • slots[].endTime — HH:MM business-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: true indicates truncation.
  • meta.returnedSlots — number of slots actually returned.
  • meta.slotLimit — the truncation limit (500).

If online booking is disabled for the business:

  • meta.availabilitySuppressed: true
  • data.slots is an empty array.

Errors#

  • availability.date_range_too_wide (400) — The dateRange is 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:

bash
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 as requested; 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 } — termsVersion exactly 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)#

StatusDescription
requestedRequest created, waiting for the business's decision
confirmedAccepted — by the business, or by the service's automatic acceptance rule (acceptanceMode)
declinedDeclined
rescheduleProposedRescheduling proposal received
cancelledCancelled
completedCompleted
noShowGuest did not show up

Lifecycle Monitoring#

Set up a webhook at the PUT /webhooks/config endpoint — you will receive notifications for every booking change.

The 8 webhook events:

  • booking.requested
  • booking.confirmed
  • booking.declined
  • booking.cancelled
  • booking.reschedule_proposed
  • booking.reschedule_confirmed
  • booking.completed
  • booking.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:

bash
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.
  • from and to (optional, in pairs): Date range filter based on the booking date.

Polling Fallback#

If webhooks are not available, you can query the booking status:

bash
curl -X GET "https://api.bokko.io/v1/bookings/{bookingId}?salonSlug=example-business" \
  -H "Authorization: Bearer bk_live_..."
Warning
Do not overload the API unnecessarily! Recommended maximum polling frequency: 1 request / 30 seconds. Use webhooks for real-time status updates.

Cancel — Cancelling a Booking (POST /bookings/{bookingId}/cancel)#

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

CodeHTTPWhen it occurs
booking.slot_unavailable409The requested time slot is no longer available.
booking.service_pricing_changed409Pricing changed between availability search and booking.
booking.slot_no_longer_available409Automatic acceptance: the final check proved the slot unavailable; no request was created.
booking.deposit_terms_required422The service requires a deposit and depositTermsAcceptance is missing.
booking.deposit_terms_changed422The accepted deposit terms version is stale.
booking.deposit_unavailable409Deposit collection is temporarily unavailable.
idempotency.payload_mismatch409Same Idempotency-Key, different payload.
idempotency.request_in_progress503Request processing is still in progress (retryable).
auth.plan_insufficient403The 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/endTime are business-local; confirmedAt and 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.