Skip to content

Book'd Out APIv1

A REST API and outbound webhooks for connecting your own tools to your Book'd Out account. Read your calendar, create bookings, sync customers, and receive events the moment they happen.

Overview

The base URL is https://www.bookdout.com/api/v1. The /api/v1 surface is stable: we only make additive changes (new endpoints, new optional fields). Breaking changes would ship as a new version prefix.

API access and webhooks are included in the Bookings Pro plan. Keys belong to one business, so every request operates on your own account and there is no account parameter to pass.

Authentication

Create keys in your admin under Settings, then Developer. Each key is shown once at creation; we store only a hash. Send the key as a bearer header on every request:

curl https://www.bookdout.com/api/v1/me \
  -H "Authorization: Bearer bko_live_your_key_here"

Rate limits

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. When you exceed a limit you get 429 rate_limited with a Retry-After header holding the seconds until the window resets.

Conventions

Errors

Errors use one envelope: { "error": "<code>", "message": "<human text>" }.

HTTPCodeMeaning
401unauthorizedMissing, malformed, or revoked key.
403plan_requiredYour plan does not include API access.
403insufficient_scopeRead-only key on a write endpoint.
404not_foundNo such resource in your account.
400/422invalid_request / validationBad parameter or body.
409slot_takenThe requested start time is not available.
409invalid_stateThe resource cannot make that transition (for example cancelling a completed booking).
422idempotency_conflictIdempotency-Key reused with a different body.
429rate_limitedRate limit hit; honour Retry-After.

Endpoint reference

EndpointScopeDescription
GET/api/v1/mereadKey introspection: business name, slug, timezone, currency, scopes.
GET/api/v1/servicesreadActive services with durations, prices and variants.
GET/api/v1/staffreadActive team members (id and name).
GET/api/v1/availabilityreadBookable slots. Params: services (comma separated ids, required), from (YYYY-MM-DD), days (max 42), staff (any or an id), variants (serviceId:variantId pairs).
GET/api/v1/appointmentsreadList appointments. Filters: from, to, status, staffId, customerId, page.
GET/api/v1/appointments/:idreadOne appointment with service, staff, customer and location.
POST/api/v1/appointmentsread_writeCreate a confirmed booking in a real slot. Supports Idempotency-Key.
POST/api/v1/appointments/:id/cancelread_writeCancel a confirmed appointment. Body: { "reason": "optional" }.
GET/api/v1/customersreadList customers. Params: q (matches name, email or phone), page.
GET/api/v1/customers/:idreadOne customer with appointment stats.
POST/api/v1/customersread_writeCreate or match a customer by email. 201 on create, 200 on match.
GET/api/v1/reviewsreadPublished first-party reviews for your business.

Create a booking

Pick a slot from /api/v1/availability and post its exact start. Pass "staffId": "any" (or omit it) to let the engine assign a team member, exactly as the online booking page does.

curl -X POST https://www.bookdout.com/api/v1/appointments \
  -H "Authorization: Bearer bko_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-12345" \
  -d '{
    "serviceId": 12,
    "staffId": "any",
    "start": "2026-08-01T00:30:00.000Z",
    "customer": { "name": "Sam Taylor", "email": "[email protected]" },
    "notifyCustomer": true
  }'

A successful create returns 201 with the full appointment. If the slot was taken in the meantime you get 409 slot_taken; fetch availability again and retry with a fresh slot. Set "notifyCustomer": false to suppress the confirmation email.

Not yet in v1 (planned): rescheduling via PATCH, sales and payment writes, managing webhook endpoints through the API, and OAuth apps. Webhook endpoints are managed in the admin for now.

Idempotency

POST /appointments and POST /customers accept an optional Idempotency-Key header (1 to 255 characters, any string unique to the operation). If a request is retried with the same key and the same body within 24 hours, the stored response is returned verbatim with the header Idempotency-Replayed: true, and no duplicate booking is created. The same key with a different body returns 422 idempotency_conflict. If the first request is still in flight you get 409 idempotency_in_progress; retry shortly.

Webhooks

Register up to 5 endpoints under Settings, then Developer. Each endpoint gets its own signing secret (whsec_...) and its own event subscription list. We POST a JSON body to your URL:

{
  "event": "appointment.created",
  "createdAt": "2026-08-01T00:30:05.123Z",
  "data": {
    "appointment": { "id": 91, "status": "confirmed", "startAtUtc": "..." },
    "changeKind": "created"
  }
}

Event types

EventFires when
appointment.createdA booking is confirmed on any channel: online, admin, mobile app, Reserve with Google, or the AI receptionist.
appointment.updatedA booking changes state or moves: rescheduled, moved by the business, marked completed or no-show. data.changeKind carries the underlying change.
appointment.cancelledA booking is cancelled by the customer or the business.
customer.createdA new customer record is created.
review.createdA customer submits a review.
sale.completedA sale is completed at the register.
Events are driven by the booking audit trail, so status changes and moves are reliable. Edits that only change fields (for example a note edit with no time or status change) are best effort and may not emit an event.

Verifying signatures

Every delivery carries three headers: Bookd-Event (the event type), Bookd-Delivery-Id (unique per delivery), and Bookd-Signature in the form t=<unix seconds>,v1=<hex HMAC>. The signature is HMAC-SHA256 of <t>.<raw body> using your endpoint secret:

const crypto = require('node:crypto');

function verify(signatureHeader, rawBody, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=')),
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > 300) return false; // reject anything older than 5 minutes
  const expected = crypto.createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(parts.v1, 'hex'),
  );
}

Retries and disabling

A non-2xx response or a timeout is retried on a backoff of 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, then 24 hours. After the final retry the delivery is marked dead; you can replay it from the delivery log in the admin. An endpoint that fails 50 deliveries in a row is disabled automatically and can be re-enabled from the same page once your receiver is fixed.

Changelog