Skip to main content
POST
Submit a booking
The only write in this API. It creates the booking, matches or creates the contact, and fires the operator notification and customer email off the response path.
The playground is read-only for this endpoint. It can’t mint the single-use Turnstile token the route requires, and firing it would write a real booking to a real tenant and burn that contact’s daily quota. Copy the request and run it against your own page.

Idempotency

Idempotency-Key is required: any caller-generated value, 1–128 printable characters. A retry with the same key returns the original booking instead of creating a second one, and the check runs before Turnstile — deliberately, because Turnstile tokens are single-use and a network retry would otherwise fail on a spent token. Generate a fresh key whenever the customer picks a different slot. That’s a different logical booking, and reusing the key would replay the old one.
A replay returns the stored booking, whatever has happened to it since. The lookup is by tenant and key only. If the operator cancelled the booking in the dashboard and the client then retries that key, booking.status comes back cancelled — which is why status spans the full set rather than just pending | confirmed. Read the status from the response rather than assuming the value you got the first time still holds.

The honeypot will swallow you silently

website is a honeypot. A non-empty value returns 200 with a believable acknowledgment and writes nothing at all. Bind it to a hidden, autocomplete="off" input and never populate it — including from a password manager or an over-eager autofill. Branch on status === 201, not on the presence of acknowledgment. The honeypot reply has no booking key precisely so the two are distinguishable.

Turnstile

You cannot use your own Turnstile key. Every booking verifies against one server-side secret, so a token minted by a different site key is rejected as turnstile_failed. Render the widget with the site key DPC issues you, and ask us to add your domain to that widget’s allowlist — otherwise the widget won’t render on your domain at all. Reset the widget after any rejected submit. The token is spent either way.

Answers

form_data is validated against the questions attached to the submitted service_id. A key this service doesn’t ask is 400 invalid_form_data — that’s what stops a stale payload, from a customer who backed up and switched service, recording answers to questions the appointment never asked. What gets stored is the normalized answer set, not your raw body: numeric strings become numbers, strings are trimmed, and an unchecked box is a real false. The client-side normalizer in the guide mirrors those rules exactly.

Headers

Idempotency-Key
string
required

Caller-generated, 1–128 printable characters. Reuse it when retrying the same booking; generate a new one when the customer picks a different slot.

Required string length: 1 - 128

Path Parameters

slug
string
required

The tenant's public booking slug. Issued by DPC; there is no discovery endpoint.

Body

application/json
service_id
string<uuid>
required
Pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
starts_at
string<date-time>
required
Pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
customer_name
string
required
Required string length: 1 - 200
customer_email
string
required
turnstile_token
string
required
Required string length: 1 - 2048
customer_phone
string | null
Maximum string length: 50
form_data
object
notes
string | null
Maximum string length: 2000
website
any

Response

Honeypot triggered — website was non-empty. Deliberately indistinguishable prose, but NO booking was created and there is no booking key. Branch on the 201 status, not on the presence of acknowledgment.

acknowledgment
string
required