> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deepcontext.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Submit a booking

> The single write. Requires an idempotency key and a Turnstile token.

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.

<Note>
  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.
</Note>

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

<Warning>
  **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.
</Warning>

## 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](/index#questions) in the guide mirrors those rules exactly.


## OpenAPI

````yaml openapi.json POST /api/public/tenants/{slug}/bookings
openapi: 3.1.0
info:
  title: DPC Bookings API
  version: 1.0.0
  description: >-
    Public, unauthenticated endpoints for taking a booking from a custom
    website. Read services, read open times, submit one booking. Booking
    management lives in the DPC dashboard.
servers:
  - url: https://deepcontext.app
security: []
paths:
  /api/public/tenants/{slug}/bookings:
    post:
      summary: Submit a booking
      description: >-
        Creates one booking. Requires an `Idempotency-Key` header and a
        Cloudflare Turnstile token minted with the site key DPC issues you — you
        cannot use your own Turnstile key, because every booking is verified
        against one server-side secret.


        A retry with the same `Idempotency-Key` returns the original booking
        rather than creating a second one, and does so before Turnstile is
        checked (those tokens are single-use).
      operationId: createBooking
      parameters:
        - name: slug
          in: path
          required: true
          description: >-
            The tenant's public booking slug. Issued by DPC; there is no
            discovery endpoint.
          schema:
            type: string
        - name: Idempotency-Key
          in: header
          required: true
          description: >-
            Caller-generated, 1–128 printable characters. Reuse it when retrying
            the same booking; generate a new one when the customer picks a
            different slot.
          schema:
            type: string
            minLength: 1
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                service_id:
                  type: string
                  format: uuid
                  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:
                  type: string
                  format: date-time
                  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:
                  type: string
                  minLength: 1
                  maxLength: 200
                customer_email:
                  type: string
                customer_phone:
                  anyOf:
                    - type: string
                      maxLength: 50
                    - type: 'null'
                form_data:
                  default: {}
                  type: object
                  propertyNames:
                    type: string
                  additionalProperties:
                    anyOf:
                      - type: string
                        maxLength: 2000
                      - type: number
                      - type: boolean
                      - type: 'null'
                sms_consent:
                  default: false
                  type: boolean
                notes:
                  anyOf:
                    - type: string
                      maxLength: 2000
                    - type: 'null'
                turnstile_token:
                  type: string
                  minLength: 1
                  maxLength: 2048
                website: {}
              required:
                - service_id
                - starts_at
                - customer_name
                - customer_email
                - turnstile_token
              additionalProperties: false
      responses:
        '200':
          description: >-
            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`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  acknowledgment:
                    type: string
                required:
                  - acknowledgment
        '201':
          description: Booking created, or the stored booking replayed for a repeated key
          content:
            application/json:
              schema:
                type: object
                properties:
                  booking:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                        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)$
                      status:
                        type: string
                        enum:
                          - pending
                          - confirmed
                          - cancelled
                          - no_show
                          - completed
                      service_id:
                        anyOf:
                          - type: string
                            format: uuid
                            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)$
                          - type: 'null'
                      starts_at:
                        type: string
                      ends_at:
                        type: string
                    required:
                      - id
                      - status
                      - service_id
                      - starts_at
                      - ends_at
                    additionalProperties: false
                  acknowledgment:
                    type: string
                required:
                  - booking
                  - acknowledgment
                additionalProperties: false
        '400':
          description: >-
            Body, email, phone, service, answers, start time, or Turnstile
            validation failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false
        '403':
          description: Browser origin is not in the tenant allowlist
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false
        '404':
          description: Unknown slug, or the tenant has booking disabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false
        '409':
          description: Another active booking owns the buffered time range
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false
        '413':
          description: Body exceeded 32 KiB
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false
        '429':
          description: Short-window IP limit, or the daily contact/IP quota
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false
        '503':
          description: An abuse gate (limiter, Turnstile, quota store) failed closed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                    required:
                      - code
                      - message
                    additionalProperties: false
                required:
                  - error
                additionalProperties: false

````