Skip to main content
Three public, unauthenticated endpoints that let a website take a booking on a tenant’s behalf. The flow is deliberately one-way — show services, show open times, submit, say thank you. Everything after that (confirming, rescheduling, cancelling, and the booking record itself) happens in the DPC dashboard, where the operator works.
Our own hosted page at /book/{slug} calls these exact endpoints, and shares its config loader and its answer validator with them. Anything the hosted page does is legal here.
If the only requirement is different styling, don’t build against this API. The hosted page already picks up the tenant’s branding, and it can be mounted on book.{customerdomain} with a single CNAME — no code in their repo. Build against the API when the flow has to be different.

Before you start

Three things are provisioned by us, per site. Ask for all three before writing code.

Tenant slug

The {slug} in every URL. There is no endpoint to discover it.

Turnstile site key

Plus your domain added to that widget’s allowlist.

Origin allowlist

Your site’s origin, added to the tenant’s allowed_origins.
You cannot bring your own Turnstile key. Every booking is verified against one server-side secret, so a token minted by a different site key is rejected with turnstile_failed. You must render the widget with the site key we give you, and we must add your domain to that widget’s allowlist — otherwise the widget refuses to render on your domain and you never get a token at all. Cross-origin submits need the allowlist. Both GET endpoints send Access-Control-Allow-Origin: * and work from anywhere. The POST compares your Origin against the tenant’s configured list and returns 403 origin_not_allowed if it isn’t there. (This is browser hygiene, not authentication — the abuse controls are the real gate.)

The whole flow

This is the entire integration.

Endpoints

Full schemas, parameters, and response shapes live in the API reference — generated from the Zod schemas the routes actually enforce, so it cannot drift from the running code. The two GET endpoints are unauthenticated and send Access-Control-Allow-Origin: *, so the playground on those pages will happily run against a real slug. The POST playground is read-only — it can’t mint a Turnstile token, and firing it would write a real booking.

Questions

Questions belong to a service, not to the tenant. Each entry in services[] carries its own form_fields, and there is no tenant-wide list. If the customer switches service, re-render from the new service’s list and drop answers the new service doesn’t ask — submitting a question id this service doesn’t ask is a hard 400 invalid_form_data. form_data is keyed by the question’s id. Five types: Optional answers left blank are simply omitted — don’t send "".
The server returns only the first validation failure, as a single string ("<question-id>: Choose one of the listed options."). To show per-field messages, mirror the table above client-side. Validating before you submit is worth the effort anyway: a server-side rejection burns the customer’s single-use Turnstile token, so they have to solve a second challenge.
This mirrors the server’s rules. Pass it the selected service’s form_fields and your raw form state; send values as form_data and render errors under each input.
Iterating fields rather than your form state is what keeps a stale answer — from a customer who switched service — out of the payload.

What to show afterwards

Branch on booking.status from the 201. A newly created booking is always one of these two, and the acknowledgment string is already worded correctly for each if you’d rather just print it. Handle the rest of the union as “we couldn’t confirm this” — a retried Idempotency-Key replays the stored booking, which the operator may have cancelled in the meantime.
Instant-booking tenant. The customer is booked, and a confirmation email is on its way. “You’re booked — check your email for the details.”
Don’t promise a confirmation email on a pending booking. The system sends exactly one customer email, at submit time. When the operator later confirms the booking in the dashboard, nothing is sent to the customer — the operator follows up themselves. Copy like “we’ll email you once it’s confirmed” is a promise the platform does not currently keep.

Errors

Every error has the same shape:
The ones a submit form should actually handle: Neither 429 sends a Retry-After header, so back off on a fixed delay.

Not in this API

There is no public read-back, cancel, or reschedule. The 201 is the only receipt a website ever gets, and a booking cannot be looked up again once the page is closed. Confirming, rescheduling, cancelling, and the full booking history all live in the DPC dashboard. Point operators there; don’t build a “manage my booking” page against this API. Server-to-server credentials — a Turnstile-free authenticated flow for non-browser clients — are not available yet.