All articles

API reference

Programmatic access to event types, bookings, availability, contacts, one-off scheduling links and webhook subscriptions. The machine-readable document behind this page is openapi.json — an OpenAPI 3.1 spec you can load into Postman, Insomnia or your own code generator.

The events Calemander sends to you are listed under Webhook events at the foot of this page. Registering an OAuth client — redirect URI rules, discovery, dynamic client registration and the scope list — is covered separately, in the developer documentation.

The /api/v1 surface of a Calemander workspace.

What a credential opens

Every endpoint is user-scoped. A credential resolves to exactly one user in one workspace, and every read is filtered to that user's own rows (WHERE user_id = ?). There is no parameter anywhere in this API for asking about a different user. The one deliberate widening is webhook subscriptions: a workspace admin also sees and manages organization-scoped subscriptions created by other members.

Which host to call

A workspace lives on its own hostname. Send requests to the origin that GET /api/v1/me reports as workspace.origin (OAuth clients are told the same thing as site_url in the token response). For the deployment's default workspace that origin is the apex domain; for every other workspace it is https://{workspace}.calemander.com.

Authentication

Two credential types, told apart by token prefix, both presented as Authorization: Bearer <token>:

  • API keycm_live_ followed by 40 hex characters. Created in the dashboard under API keys with the scopes you tick and an optional expiry; only its hash is stored, so the plaintext is shown exactly once. A key opens the workspace it was created in and no other, acts as the member who created it, and stops working when that person leaves the workspace or when it expires. Keys were reset in September 2026 when they moved to resource scopes: any key created before then no longer works and must be re-created.
  • OAuth 2.0 access tokencmo_at_ prefix, issued by the authorization-code flow with PKCE. Access tokens live one hour; refresh tokens ninety days.

Both carry the same scopes and both resolve to the same {userId, scopes}, so no endpoint behaves differently depending on which one you used.

Scopes

Scopes are resource:action strings and a credential holds exactly the ones it was granted — there is no implication ladder (bookings:write does not include bookings:read; tick both). Every operation below names the scope it requires. A request whose credential lacks it is refused with 403 and a message naming the scope (Insufficient scope. Required: bookings:write).

| Scope | Opens | |---|---| | bookings:read | GET /bookings — your bookings, with each attendee's name, email and notes | | bookings:write | POST /bookings, PATCH /bookings/{id}, POST/DELETE /bookings/{id}/no-show | | event_types:read | GET /event-types | | event_types:write | POST /event-types, PATCH /event-types/{id} | | availability:read | GET /availability/{slug} | | contacts:read | GET /contacts — names, emails, phone numbers, companies | | scheduling_links:read | GET /one-off-links | | scheduling_links:write | POST /one-off-links | | webhooks:read | GET /webhooks, GET /webhooks/{id} | | webhooks:manage | POST /webhooks, DELETE /webhooks/{id} — your own subscriptions; an organization-scoped subscription additionally needs the credential's owner to be a workspace admin | | check_in:read | GET /api/check-in/sessions, GET /api/check-in/sessions/{id} (the check-in API, outside /api/v1) | | check_in:write | check-in, walk-in and undo on a session | | check_in:manage | no-show, close and reopen a session |

GET /api/v1/me needs no particular scope: any valid credential may ask what it is.

A credential never exceeds its owner's role. Scopes are checked against the member's live workspace role on every request, so a change of role applies to the next call. Nothing in this API reaches workspace settings, members or billing, whatever the credential holds. Older OAuth grants that were consented to with the legacy words read / write keep exactly the access those words opened at the time (read = every *:read above except check-in; write adds bookings:write, scheduling_links:write, webhooks:manage); the consent screen shows new grants as the resource scopes they resolve to.

Rate limiting

60 requests per minute per credential, counted in a fixed 60-second window. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp in seconds). Over the limit the API answers 429 with a Retry-After header.

Error bodies

Almost every error is {"message": "..."}. The single exception is the 429, which is {"error": "Rate limit exceeded", "retryAfter": <unix seconds>} — it is produced before routing, by the rate limiter, rather than by a handler.

Times

All timestamps are UTC. Booking times are returned as they are stored; POST /api/v1/bookings normalises the times you send to UTC before persisting them.

Endpoints

Identity

Who the calling credential belongs to.

GET /api/v1/me

Identify the calling credential

Returns the user the credential opens, the workspace it belongs to, and how it authenticated. Everything in the response is derived from the credential itself; there is no way to ask about another user.

This is the connection-test endpoint: it is what an integration calls once at connection time to check a credential is live and to build a human-readable label for the connection.

Responses

StatusBodyDescription
200Identity

The credential's identity.

401Error

The credential is missing, invalid, or no longer resolves to a user.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "user": {
    "id": "usr_9f21",
    "email": "ada@example.com",
    "name": "Ada Lovelace",
    "slug": "ada",
    "timezone": "Europe/London",
    "role": "admin",
    "scheduling_url": "https://acme.calemander.com/ada"
  },
  "workspace": {
    "slug": "acme",
    "origin": "https://acme.calemander.com"
  },
  "auth": {
    "method": "api_key",
    "scopes": [
      "bookings:read",
      "bookings:write",
      "event_types:read"
    ]
  },
  "api_key": {
    "id": "key_1",
    "name": "Zapier",
    "prefix": "cm_live_a1b2",
    "scopes": [
      "bookings:read",
      "bookings:write",
      "event_types:read"
    ],
    "expires_at": "2027-08-01T00:00:00.000Z",
    "created_at": "2026-08-01T00:00:00.000Z"
  }
}

Event Types

The bookable meeting templates a user offers.

GET /api/v1/event-types

List event types

Requires the event_types:read scope.

The authenticated user's own event types, newest first. Soft-deleted event types are never returned. Inactive ones are excluded unless include_inactive=true.

hosts is populated only for round_robin and collective event types; a one-on-one event type reports an empty list.

Parameters

NameInTypeDescription
include_inactive query"true" | "false"Include event types with isActive: false. Only the exact string true enables this; any other value is treated as false.
page queryinteger1-based page number. Values below 1 and unparseable values fall back to 1.
limit queryintegerRows per page. Clamped to 1–100; unparseable values fall back to 50.

Responses

StatusBodyDescription
200

A page of event types.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/event-types

Create an event type

Requires the event_types:write scope.

Creates an event type owned by the authenticated user, through the same validator the dashboard's form uses (slug rules, length limits, member checks, plan caps). The 201 body is the created event type in the same shape GET /api/v1/event-types returns.

On a plan whose active-event-type cap is full, the event type is created switched off rather than refused, and the body carries deactivatedByPlan: true with planMessage — surface that to the user, because an inactive event type is a booking page that simply does not exist.

A new group event type (maxSeats above 1) on a plan with check-in starts with a 10-minute auto-close and credits spent at attendance; on other plans, or for a one-on-one, it starts with no auto-close and booking-time credits. Pass autoCloseMinutes / creditTiming to set them explicitly.

Request body (required)

FieldTypeDescription
{
  "name": "Morning Flow",
  "slug": "morning-flow",
  "durationMinutes": 60,
  "maxSeats": 12
}

Responses

StatusBodyDescription
201

Created.

400Error

Body was not a JSON object, a field has the wrong type or an unknown name, a required field is missing, the slug is malformed or already taken, a member id is not in this workspace, or a check-in value is out of range.

401Error

No credential was presented, or it did not resolve.

402Error

autoCloseMinutes or creditTiming: attendance was requested on a plan without check-in.

403Error

The credential lacks the event_types:write scope, or a team meeting mode was requested by a user who is not a workspace admin.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

PATCH /api/v1/event-types/{id}

Update an event type

Requires the event_types:write scope.

Changes only the fields present in the body, on one of the authenticated user's own event types. An event type that exists but belongs to another member is 404, the same as one that does not exist. The 200 body is the updated event type in the GET /api/v1/event-types shape.

Parameters

NameInTypeDescription
id *pathstringThe event type id.

Request body (required)

FieldTypeDescription
{
  "name": "Evening Flow",
  "isActive": true
}

Responses

StatusBodyDescription
200

Updated.

400Error

Body was not a JSON object or had nothing to update, a field has the wrong type or an unknown name, the slug is malformed, a member id is not in this workspace, memberIds is missing for a switch to a team mode, or a check-in value is out of range.

401Error

No credential was presented, or it did not resolve.

402Error

Switching the event type on would exceed the plan's active-event-type cap, or auto-close / attendance credits were requested on a plan without check-in.

403Error

The credential lacks the event_types:write scope, or the meeting mode or hosts were changed by a user who is not a workspace admin.

404Error

No event type with this id is owned by the authenticated user.

409Error

The new slug is already used by another event type in the workspace.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

Bookings

Scheduled meetings: list, create, and change status.

GET /api/v1/bookings

List bookings

Requires the bookings:read scope.

The authenticated user's own bookings, most recent start time first. All filters combine with AND.

Parameters

NameInTypeDescription
status querystringExact match on the stored status. Not validated against a list — an unknown value simply matches nothing. Values in use: confirmed, pending, canceled, rescheduled, payment_pending, no_show.
event_type_id querystringOnly bookings for this event type.
from querystringLower bound on startTime, inclusive. Compared as a string against the stored UTC value, so pass the same ISO 8601 shape the API returns.
to querystringUpper bound on startTime, inclusive. Same string-comparison note as from.
payment_provider querystringExact match on which rail holds this booking's money. external means your own checkout collected it — ?status=payment_pending&payment_provider=external is "list unpaid reservations".
attendee_email querystringExact match on the attendee's email, case-insensitively. Not a substring search: a@b.com will not match aa@b.com.
page queryinteger1-based page number. Values below 1 and unparseable values fall back to 1.
limit queryintegerRows per page. Clamped to 1–100; unparseable values fall back to 50.

Responses

StatusBodyDescription
200

A page of bookings.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings

Create a booking

Requires the bookings:write scope.

Books a slot on one of the authenticated user's own event types, bypassing the public booking page.

The slot is claimed before the booking row is written, so a time that another booking already occupies is refused with 409 rather than double-booked. Claiming happens only for statuses that occupy a slot (confirmed, rescheduled, pending, payment_pending).

What this endpoint does not do: it does not send confirmation email, create a calendar event, provision a conferencing link, schedule reminders, or fire booking.created webhooks. Those belong to the public booking flow. A booking created here has meetingUrl: null and attendeeTimezone recorded as UTC, and guest emails are always empty.

The 201 body echoes the startTime and endTime as you supplied them, not as they were normalised and stored. Read the booking back if you need the stored form.

Hold-gate requirements. If the event type has a requirement whose gate is hold, this endpoint refuses with 409 unless you send allowUnpaidHold: true. The public booking page has no such escape hatch: a hold is a real seat out of availability, and the gate exists so a stranger cannot take one on a prerequisite only you can satisfy.

Request body (required)

FieldTypeDescription
eventTypeId *stringMust be an active, non-deleted event type owned by the authenticated user.
startTime *stringMeeting start. Anything the runtime can parse as a date-time; normalised to UTC before it is stored.
endTime *stringMeeting end. Normalised to UTC before it is stored.
attendeeName *string
attendeeEmail *string
notes stringFree text stored as the booking's attendee notes.
status stringStatus to create the booking in. Not validated — whatever string you send is stored. Omit it to get pending when the event type requires confirmation and confirmed otherwise.
allowUnpaidHold booleanCreate the booking even though the event type has a requirement at the hold gate. Without it the request is refused with 409, the same as the public booking page. The seat is genuinely taken and the requirement stays outstanding, so the booking is held and not confirmed; the credential that sent this is recorded on the booking as the one that authorized it.
{
  "eventTypeId": "et_intro30",
  "startTime": "2026-09-14T15:00:00Z",
  "endTime": "2026-09-14T15:30:00Z",
  "attendeeName": "Grace Hopper",
  "attendeeEmail": "grace@example.com",
  "notes": "Wants to discuss the pilot."
}

Responses

StatusBodyDescription
201

Booking created.

400Error

A required field is missing, the email is not valid, or a time could not be parsed.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No active, non-deleted event type with that id belongs to the authenticated user.

409Error

The slot could not be claimed — something else holds that time.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

PATCH /api/v1/bookings/{id}

Update a booking's status or notes

Requires the bookings:write scope.

Changes the status and/or the attendee notes of one of the authenticated user's own bookings. Send at least one of the two fields.

The status write is conditional on the status you last read: if the booking moved underneath you, the update is refused with 409 rather than applied on top of a state you did not see.

Cancelling through this endpoint carries the same side effects as cancelling in the dashboard. Moving a booking to canceled releases its slot, deletes the linked Google, Outlook, Zoom and CalDAV entries (best effort — provider failures are logged, not surfaced), cancels pending scheduled emails, and sends the attendee the cancellation email if email is configured and the template is enabled.

Status transitions fire webhooks: booking.canceled, booking.confirmed or booking.no_show for the new status, and additionally booking.no_show_cleared whenever the booking is moving off no_show.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Request body (required)

FieldTypeDescription
status "confirmed" | "canceled" | "pending" | "no_show"The only four values this endpoint accepts. Anything else is a 400.
attendeeNotes string or nullReplaces the notes. An empty string clears them.
{
  "status": "canceled"
}

Responses

StatusBodyDescription
200

Booking updated.

400Error

The status is not one of the four allowed values, or neither field was supplied.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No booking with that id belongs to the authenticated user.

409Error

Either the slot this status change would occupy is already taken, or the booking's status changed between your read and this write. Re-read and retry.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "success": true,
  "id": "bkg_4c21"
}

POST /api/v1/bookings/{id}/no-show

Mark an attendee as a no-show

Requires the bookings:write scope.

Records that the attendee did not show up, moving the booking to no_show. The request takes no body — the booking is named in the path.

Only a confirmed booking whose start time has already passed can be marked: a no-show is an observation about a meeting that happened, so a booking in any other status, or one still in the future, is refused with 400.

This is the same operation the dashboard performs, and it is deliberately not the same as PATCH /api/v1/bookings/{id} with status: no_show. It fires the booking.no_show webhook *and* runs the host’s booking.no_show automation rules, exactly as a mark made in the dashboard does.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Responses

StatusBodyDescription
200NoShowResult

The booking is now marked as a no-show.

400Error

The booking is not confirmed, or its start time is still in the future.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No booking with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "success": true,
  "id": "bkg_4c21",
  "status": "no_show"
}

DELETE /api/v1/bookings/{id}/no-show

Clear an attendee’s no-show mark

Requires the bookings:write scope.

Takes a no-show mark back, returning the booking to confirmed. A booking not currently marked no_show is refused with 400.

Clearing is its own operation rather than a plain status write because it is its own event to a subscriber: it fires booking.no_show_cleared, so whatever acted on the original booking.no_show learns the mark was withdrawn. Automations do not re-run — there is no un-sending an email an automation already sent.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Responses

StatusBodyDescription
200NoShowResult

The no-show mark has been withdrawn.

400Error

The booking is not marked as a no-show.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No booking with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "success": true,
  "id": "bkg_4c21",
  "status": "confirmed"
}

POST /api/v1/bookings/{id}/retime-link

Mint a re-time link

Requires the bookings:write scope.

For a released hold whose link expired, or one you want to hand over another way. Minting replaces any earlier link for that booking, and nothing is emailed — you decide how it reaches the person.

Only a hold the release rule ended can be re-timed. A booking you cancelled yourself gets a one-off scheduling link instead.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Request body

FieldTypeDescription
expiresInHours integerDefaults to the event type's retimeWindowHours, or 336 when that is 0.

Responses

StatusBodyDescription
201

The link and when it expires.

400Error

invalid_body: expiresInHours is out of range.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No booking with that id belongs to the authenticated user.

409Error

not_released, or already_retimed when the attendee already picked a new time.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

Availability

Bookable slots for one event type on one date.

GET /api/v1/availability/{slug}

List bookable slots for one date

Requires the availability:read scope.

Bookable start/end pairs for one event type on one calendar date, in UTC.

Slots account for the owner's weekly availability rules (or that date's overrides, which take precedence), existing bookings, the event type's buffers, minimum notice and slot interval, and the owner's booking-frequency caps. An empty slots array is a normal answer: it means the date is fully booked, capped, overridden closed, or has no rules for that weekday.

Results are cached briefly per slug and date.

Parameters

NameInTypeDescription
slug *pathstringEvent type slug. Must be active, not deleted, and owned by the authenticated user.
date *querystringThe date to check, YYYY-MM-DD. Rejected with 400 if absent or not that exact shape.

Responses

StatusBodyDescription
200

Bookable slots for that date.

400Error

date is missing or is not YYYY-MM-DD.

401Error

No credential was presented, or it did not resolve.

403Error

The event type exists but belongs to a different user, or the credential lacks the availability:read scope.

404Error

No active, non-deleted event type has that slug.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "slots": [
    {
      "start": "2026-09-14T15:00:00.000Z",
      "end": "2026-09-14T15:30:00.000Z"
    },
    {
      "start": "2026-09-14T16:00:00.000Z",
      "end": "2026-09-14T16:30:00.000Z",
      "best": true
    }
  ]
}

Contacts

People who have booked with this user.

GET /api/v1/contacts

List contacts

Requires the contacts:read scope.

The authenticated user's own contacts, newest first.

Parameters

NameInTypeDescription
search querystringCase-insensitive substring match against name, email or company. % and _ in the value are SQL LIKE wildcards and are not escaped.
tag querystringOnly contacts carrying this tag. Matched against the stored JSON tag list, so the match is on the whole tag, not a fragment of one.
page queryinteger1-based page number. Values below 1 and unparseable values fall back to 1.
limit queryintegerRows per page. Clamped to 1–100; unparseable values fall back to 50.

Responses

StatusBodyDescription
200

A page of contacts.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

One-Off Links

Single-use scheduling links minted for one invitee.

List one-off scheduling links

Requires the scheduling_links:read scope.

The authenticated user's own one-off links, newest first.

Not paginated. The response is capped at the 100 most recent links and there is no page, limit or total.

Known behaviour: a link whose event type has since been soft-deleted is still listed here. The link itself stops working — booking it returns 404 — but it remains in this listing.

Parameters

NameInTypeDescription
event_type_id querystringOnly links for this event type.

Responses

StatusBodyDescription
200

Up to 100 links.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/one-off-links

Mint a one-off scheduling link

Requires the scheduling_links:write scope.

Creates a scheduling link that stops accepting bookings after max_uses bookings — one, by default. Built for post-payment automation: mint a link for a single customer, send it to them, and let it retire itself.

Identify the event type by event_type_id or event_type_slug; send at least one. Either way it must be an active, non-deleted event type owned by the authenticated user.

The 201 body is deliberately narrow: it does not repeat the raw token, the use count, or the creation timestamp. Call GET /api/v1/one-off-links for the full record.

Request body (required)

FieldTypeDescription
event_type_id string
event_type_slug stringUsed only when event_type_id is absent.
label stringFree text to identify the link in listings. Not shown to the invitee.
expires_at stringWhen the link stops working. Must parse as a date and be in the future.
max_uses integerHow many bookings the link accepts before it is spent.
{
  "event_type_slug": "onboarding-call",
  "label": "Order 10482",
  "max_uses": 1
}

Responses

StatusBodyDescription
201

Link created.

400Error

Body was not JSON, neither identifier was supplied, expires_at did not parse or is in the past, or max_uses is outside 1–1000.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No active, non-deleted event type matching that id or slug belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "id": "9f2c41ab7d5e4c0f8b1a",
  "url": "https://acme.calemander.com/book/6f1d7c22-0b3e-4a91-9a4c-1f0e8b2d3c44",
  "label": "Order 10482",
  "expires_at": null,
  "max_uses": 1
}

Webhooks

Subscriptions that deliver booking events to your endpoint.

GET /api/v1/webhooks

List webhook subscriptions

Requires the webhooks:read scope.

Subscriptions the authenticated user can see: their own, plus — if they are a workspace admin — every organization-scoped subscription in the workspace regardless of who created it. Newest first.

Not paginated: every visible subscription is returned in one array.

Responses

StatusBodyDescription
200

Visible subscriptions.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/webhooks

Create a webhook subscription

Requires the webhooks:manage scope.

Subscribes an HTTPS endpoint to one or more events.

The signing secret is returned only here, on creation. It is never included in any later response, and there is no way to read it back or rotate it — to change it, delete the subscription and create another.

There is no update endpoint by design. Delete and recreate.

Request body (required)

FieldTypeDescription
url *stringWhere deliveries are POSTed. Must be https: — the only exception is a loopback address (localhost, 127.0.0.1, [::1]), which may use http: for local development.
events *array of WebhookEventNameEvents to receive. Every entry must be a known event name; one unknown name rejects the whole request.
scope "user" | "organization"user fires only for bookings this user hosts. organization fires for every host in the workspace, and may only be created by a workspace admin.
payload_format "calemander" | "calendly"The wire format this endpoint receives. See the webhooks section for both shapes.
{
  "url": "https://hooks.example.com/calemander",
  "events": [
    "booking.created",
    "booking.canceled",
    "booking.rescheduled"
  ],
  "scope": "user",
  "payload_format": "calemander"
}

Responses

StatusBodyDescription
201WebhookSubscription & object

Subscription created. This is the only response that carries secret.

400Error

Body was not JSON, url is missing or not HTTPS, events is missing or empty, an event name is unknown, or scope / payload_format is not a recognised value.

401Error

No credential was presented, or it did not resolve.

403Error

The credential lacks the webhooks:manage scope, or an organization scope was requested by a user who is not a workspace admin.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "id": "wh_123",
  "url": "https://hooks.example.com/calemander",
  "events": [
    "booking.created",
    "booking.canceled",
    "booking.rescheduled"
  ],
  "scope": "user",
  "payload_format": "calemander",
  "is_active": true,
  "created_at": "2026-08-29T09:14:22Z",
  "secret": "0f1c9d6a-8b52-4c1e-9f7a-2d3b4c5e6f70"
}

GET /api/v1/webhooks/{id}

Fetch one webhook subscription

Requires the webhooks:read scope.

A subscription the authenticated user manages: their own, or — for a workspace admin — any organization-scoped one.

A subscription that exists but that this user may not manage returns 404, the same as one that does not exist.

This response omits payload_format, which the list and create responses both include. Read it from GET /api/v1/webhooks if you need it. The signing secret is never returned here.

Parameters

NameInTypeDescription
id *pathstringSubscription id.

Responses

StatusBodyDescription
200WebhookSubscriptionDetail

The subscription.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such subscription, or it is not one this user manages.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

DELETE /api/v1/webhooks/{id}

Delete a webhook subscription

Requires the webhooks:manage scope.

Removes the subscription permanently. Same visibility rule as the fetch: a subscription this user does not manage returns 404.

Parameters

NameInTypeDescription
id *pathstringSubscription id.

Responses

StatusBodyDescription
200

Deleted.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such subscription, or it is not one this user manages.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

{
  "success": true
}

Theme

The booking page's appearance, and the proposals that come from reading a website.

GET /api/v1/theme

Read the booking page theme

Requires the theme:read scope.

The six structured theme columns for the authenticated user. custom_css is never returned.

Responses

StatusBodyDescription
200

The current theme.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

PATCH /api/v1/theme

Change the booking page theme

Requires the theme:write scope.

Writes only the fields you send; anything absent keeps its stored value. The merged result is validated the same way the dashboard validates it, so a font outside the catalog, a malformed color or a radius above 32px is refused.

A body containing custom_css is refused with 400 and code: custom_css_not_writable_via_api — never silently dropped.

Request body (required)

FieldTypeDescription
theme_preset stringPanel preset: glass, glass-dark, minimal, colorful or dark.
accent_color stringButton and link color, #rrggbb.
font_family stringA font from the catalog. Any other name is refused.
border_radius stringCorner radius, 0px to 32px.
background_type stringgradient, solid, preset, image or video.
background_value stringThe color, preset name or URL the type calls for.

Responses

StatusBodyDescription
200

The theme as it now is.

400

Invalid field, or custom_css was sent.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/theme/match

Read a website and propose a theme from it

Requires the theme:write scope.

Fetches the page at url and its stylesheets, and returns colors, a font, a button shape and a logo candidate, stored as a proposal you can apply. Nothing changes until you apply it.

The read is bounded: five a minute and forty a day per workspace, shared with the dashboard. A read that fails is not an error — the 200 body carries ok: false and a fetch_note saying whether the address was refused, unreachable or blocked.

The site is fetched with no cookies, no credentials and no headers of yours, and nothing read from it becomes CSS.

Request body (required)

FieldTypeDescription
url *stringThe public https address of the website to read.

Responses

StatusBodyDescription
200

A proposal, or a note saying why the site could not be read.

400Error

No url, or one too long to be a website address.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

GET /api/v1/theme/proposals

List theme proposals

Requires the theme:read scope.

This workspace's own match history, newest first. Summaries only; read one proposal for its palette and fields.

Parameters

NameInTypeDescription
page queryinteger1-based page number. Values below 1 and unparseable values fall back to 1.
limit queryintegerRows per page. Clamped to 1–100; unparseable values fall back to 50.

Responses

StatusBodyDescription
200

One page of history.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

GET /api/v1/theme/proposals/{id}

Read one theme proposal

Requires the theme:read scope.

The stored palette and proposal. A proposal belonging to another user reads as 404.

Parameters

NameInTypeDescription
id *pathstringThe proposal id, from a match or from the history list.

Responses

StatusBodyDescription
200

The proposal as stored.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such proposal for this user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/theme/proposals/{id}/apply

Apply a theme proposal

Requires the theme:write scope.

Writes the accepted fields to the booking page and snapshots what was there, so the change can be reverted exactly. Send fields to accept some of them; omit it to accept every field the proposal carries.

overrides.custom_css is refused with 422 and code: custom_css_not_writable_via_api. A proposal never carries custom CSS and this path cannot write it.

Parameters

NameInTypeDescription
id *pathstringThe proposal to apply.

Request body

FieldTypeDescription
fields array of stringProposal field names to accept, e.g. accent_color. Omit to accept all of them.
overrides objectYour own values, applied over the proposal. Theme fields only.

Responses

StatusBodyDescription
200

The theme as it now is.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

409Error

The proposal was already applied.

422

The proposal cannot be applied, or custom_css was sent.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/theme/proposals/{id}/revert

Undo an applied theme proposal

Requires the theme:write scope.

Restores the theme snapshot taken when the proposal was applied, exactly as it was, and marks the proposal reverted. Only an applied proposal can be reverted.

Parameters

NameInTypeDescription
id *pathstringThe applied proposal to undo.

Responses

StatusBodyDescription
200

The restored theme.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

409Error

The proposal was never applied, or has no snapshot.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/theme/preview-link

Mint a theme preview link

Requires the theme:write scope.

Reads a website and returns a signed, short-lived link showing a demo workspace's real service menu in that site's colors, watermarked as an example. Nothing is stored and no booking made on it is real.

The read costs one unit from the match budget and one from the preview budget. A deployment with no demo workspace answers 503.

Request body (required)

FieldTypeDescription
url *stringThe public https address of the website to read.

Responses

StatusBodyDescription
200

A preview link and when it stops working.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

422

The site could not be read.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

503Error

No demo workspace is configured.

Requirements

The prerequisites a booking is held on, and the clears that release it.

POST /api/v1/checkout-claims

Mint a reservation token for your own checkout

Requires the bookings:write scope.

Asks for a short-lived, signed token that lets ONE customer reserve ONE time on ONE event type from inside your own checkout.

Call it server-side, when the customer reaches your checkout and you are about to show them the time picker. Put the returned embedUrl in an iframe; when the customer picks a time the booker reserves it immediately — off your calendar, no email, no webhook — and posts calemander:claimCreated to your page with the booking id to carry on your payment.

The event type must carry a requirement marked as the one your checkout reserves against ("Payment is collected on my site"), and must not also carry a Calemander price, deposit or card-on-file setting: two rails charging one booking is two sources of truth for the same money.

claimTtlSeconds is how long the RESERVATION lasts once it is made — set it to the life of your own payment session. A reservation that outlives the session holds inventory nobody can buy; one that dies before it produces 410s when the payment finally lands. tokenTtlSeconds is the different, shorter clock on how long this token may be used to START a reservation.

Bind attendeeEmail whenever you know it: it is what stops a token leaked out of one customer's browser reserving a time for somebody else. Do not log the token, and do not mint one before you know the customer is on the page.

Request body (required)

FieldTypeDescription
eventTypeId *stringThe event type the customer is buying. Must carry a checkout requirement.
requirementKey stringThe checkout requirement's key. Optional; refused if it is not the one this event type carries.
attendeeEmail stringBind the token to one address. The booking must then be made with it.
claimTtlSeconds integerHow long the reservation lasts once made, 300–7200. Default 1800.
tokenTtlSeconds integerHow long this token may be used to start a reservation, 300–3600. Default 1800.

Responses

StatusBodyDescription
201

The token, its expiry, and the embed URL to mount.

400Error

The event type has no checkout requirement, carries a Calemander price, or a TTL is out of range.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such event type in this workspace.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings/{id}/external-payment

Record a payment you took on your own site

Requires the requirements:write scope.

Tells Calemander that the payment for a reserved time cleared. Call it from your payment webhook, or wherever you learn the money arrived.

Calemander charged nobody: it records "reported paid by <provider> under <reference>", marks the booking paid, fires booking.created at THIS moment — not when the time was reserved — and then either confirms the booking or holds it on whatever else you require. provider is a label of your own choosing (stripe, square, invoice, manual, anything); nothing branches on it.

Send an Idempotency-Key. Your provider will deliver its webhook more than once, and with the key the second delivery gets the first answer verbatim, status code included, with nothing running twice.

A 410 means the reservation ended before the payment arrived and the time may already be sold. The payment is still recorded, so the audit trail is honest; Calemander does not resurrect the booking and never refunds anything. What to tell the customer is yours to decide — the pattern that works is a one-off scheduling link and one true sentence.

The generic requirement clear endpoint refuses the checkout requirement of a live reservation with 409 record_payment_instead: "paid" is a money fact with a provider, an amount and a currency, and it belongs on the booking.

Parameters

NameInTypeDescription
id *pathstringBooking id — the one the booker gave you when the time was reserved.

Request body (required)

FieldTypeDescription
provider *stringYour label for who took the money. 1–40 characters of lowercase letters, digits, hyphens or underscores.
reference *stringYour own reference for the payment, 1–200 characters.
amountCents integerWhat was paid, in the event type's currency.
currency stringMust match the event type's currency, or be omitted.
receiptUrl stringA link a person can follow to the receipt.
paidAt stringWhen the payment cleared, ISO-8601.
note stringA line for the host. Never shown to the customer.

Responses

StatusBodyDescription
200

Recorded, or already recorded. bookingStatus says whether the booking is confirmed or held on something else.

400Error

The body cannot be acted on: no provider or reference, a bad amount, or the wrong currency.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No reservation with that id in this workspace, or the booking was not made through your checkout.

409Error

The same payment is already being recorded under this Idempotency-Key.

410Error

The reservation ended before the payment arrived. Recorded, not resurrected, not refunded.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings/{id}/release-claim

Give a reservation back

Requires the bookings:write scope.

Puts a reserved time back on sale now, rather than waiting for it to expire.

Optional: a customer who picks again replaces their own reservation, and an unpaid one expires on its own. Use this when your checkout KNOWS the customer left.

released: false is not an error — it means the reservation was already gone, or already paid for. A payment landing in the same instant wins, and the time stays sold.

Parameters

NameInTypeDescription
id *pathstringBooking id of the reservation to give back.

Responses

StatusBodyDescription
200

Whether this call was the one that released it.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such booking in this workspace.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

GET /api/v1/bookings/{id}/requirements

List what a booking is still waiting on

Requires the requirements:read scope.

The booking’s requirement checklist in the order an attendee reads it, with the full provenance of everything already satisfied.

Only cleared and waived count as met. A booking whose outstanding is 0 is confirmed; one with rows still open is held — its time is taken out of availability, and there is no calendar event, no reminder and no confirmation email until the last requirement clears.

A booking with an empty list is not a requirements booking and behaves exactly as it always has.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Responses

StatusBodyDescription
200BookingRequirementList

The booking’s requirements.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No booking with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings/{id}/requirements/{key}/clear

Clear a requirement

Requires the requirements:write scope.

Records that a prerequisite has been met, from the system that actually knows. If it was the last one, the booking confirms itself: calendar event, confirmation email, reminders, and the booking.confirmed webhook, with nobody clicking anything.

Send externalRef: the identifier your system matched on. How the clear is classified is not yours to send. There is no clearMethod field and there must not be — a caller that could name its own method could dress a guess up as a county’s reference. A requirement that was outstanding records system_reference; one the attendee had already submitted records attendee_evidence with verifiedBy: "api", and referenceMatched is true when your reference is the one they typed.

Clearing an already-cleared requirement is a 200 no-op: the answer is already yes. Clearing on a canceled or released booking is also a 200 — the clear is recorded, the booking is not resurrected, and the response says what the booking’s status actually is.

Idempotency. Send an Idempotency-Key header and the write runs once: a replay with the same key returns the first response verbatim rather than acting again, so a retry loop cannot fire booking.confirmed twice. A clear carrying an externalRef that already sits on a cleared requirement of the same booking is a no-op regardless of the header.

Parameters

NameInTypeDescription
id *pathstringBooking id.
key *pathstringThe requirement’s key, as defined on the event type.

Request body

FieldTypeDescription
externalRef stringThe identifier your system matched on — a county application number, a policy number, a certificate id.
evidenceUrl stringA link your system can supply for a person who wants to look at the underlying record.
note stringA line for the host. Never shown to the attendee.

Responses

StatusBodyDescription
200RequirementWriteResult

The requirement’s state after the call, and the booking’s.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such booking for this credential, or the booking has no requirement with that key.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings/{id}/requirements/{key}/waive

Waive a requirement

Requires the requirements:write scope.

Decides that this booking does not need the prerequisite after all. Waiving satisfies, so it can be the move that confirms the booking.

note is required. A waiver is a person overriding a gate, and a waiver with no reason is an unexplained hole in the record — it is the one line a host reading the booking six weeks later needs. The note is host-facing and is never rendered to the attendee.

Idempotency. Send an Idempotency-Key header and the write runs once: a replay with the same key returns the first response verbatim rather than acting again, so a retry loop cannot fire booking.confirmed twice. A clear carrying an externalRef that already sits on a cleared requirement of the same booking is a no-op regardless of the header.

Parameters

NameInTypeDescription
id *pathstringBooking id.
key *pathstringThe requirement’s key, as defined on the event type.

Request body (required)

FieldTypeDescription
note *stringWhy this booking does not need it. Required.

Responses

StatusBodyDescription
200RequirementWriteResult

The requirement is waived.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such booking for this credential, or the booking has no requirement with that key.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings/{id}/requirements/{key}/verify

Verify what the attendee submitted

Requires the requirements:write scope.

Says yes to a reference the attendee supplied themselves. Only a submitted requirement can be verified; anything else is a 409 naming its state, because a caller that meant to verify and instead cleared an untouched requirement would have recorded something stronger than what happened.

The attendee submitting a reference satisfies nothing on its own: it is a claim about the outside world, and a booking must never confirm on a claim. This is the second party saying the claim is true. Send your own externalRef and referenceMatched comes back true when it is the same one they typed — the strongest signal the system holds. When the two disagree the requirement still clears and referenceMatched stays false: a person can mistype while the outside system approves the right file, and a hard block there would hold a correct booking hostage to a typo. It is surfaced, not enforced.

Idempotency. Send an Idempotency-Key header and the write runs once: a replay with the same key returns the first response verbatim rather than acting again, so a retry loop cannot fire booking.confirmed twice. A clear carrying an externalRef that already sits on a cleared requirement of the same booking is a no-op regardless of the header.

Parameters

NameInTypeDescription
id *pathstringBooking id.
key *pathstringThe requirement’s key, as defined on the event type.

Request body

FieldTypeDescription
externalRef stringThe identifier your system matched on, compared against what the attendee typed.
note stringA line for the host. Never shown to the attendee.

Responses

StatusBodyDescription
200RequirementWriteResult

The submitted reference is verified and the requirement is cleared.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such booking for this credential, or the booking has no requirement with that key.

409Error

The requirement is not in the submitted state.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/bookings/{id}/requirements/{key}/send-back

Send an attendee’s submission back

Requires the requirements:write scope.

Rejects a reference the attendee supplied. The requirement moves to failed, which does not satisfy, so the booking stays held — and the host sees why, instead of an outstanding row that looks like nobody has looked at it.

note is required and is written for the host; it is never shown to the attendee. Sending back also invalidates the attendee’s outstanding self-serve links, so they open a fresh one and retype. The ordinary cause of a rejection is a mistyped digit, and that should cost one email rather than a support ticket.

Idempotency. Send an Idempotency-Key header and the write runs once: a replay with the same key returns the first response verbatim rather than acting again, so a retry loop cannot fire booking.confirmed twice. A clear carrying an externalRef that already sits on a cleared requirement of the same booking is a no-op regardless of the header.

Parameters

NameInTypeDescription
id *pathstringBooking id.
key *pathstringThe requirement’s key, as defined on the event type.

Request body (required)

FieldTypeDescription
note *stringWhat was wrong with it. Required, and host-facing only.

Responses

StatusBodyDescription
200RequirementWriteResult

The submission has been sent back.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such booking for this credential, or the booking has no requirement with that key.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/requirements/preclear

Record a clear that arrived before the booking

Requires the requirements:write scope.

Files a clear against a person rather than a booking, for when the outside system knows before the attendee has picked a time.

The case this exists for: an approval lands while the customer has still not booked. There is no requirement row to clear, and asking your integration to poll until one appears would trade a fact it already holds for a schedule. Post it here instead. When a booking is later created for a matching subject, the requirement is written already cleared, carrying this call’s provenance, and the booking confirms on the spot.

subject needs at least one of attendeeEmail or externalRef — a pre-clearance naming nobody would not mean "this customer is cleared", it would mean "clear the next booking that arrives". A pre-clearance is single-use and expires (180 days unless the requirement’s definition says otherwise), because an approval about one file should not confirm a second booking, and should not still be confirming bookings after the thing it attested to has lapsed.

Idempotency. Send an Idempotency-Key header and the write runs once: a replay with the same key returns the first response verbatim rather than acting again, so a retry loop cannot fire booking.confirmed twice. A clear carrying an externalRef that already sits on a cleared requirement of the same booking is a no-op regardless of the header.

Request body (required)

FieldTypeDescription
key *stringThe requirement key this clears.
eventTypeId stringLimit it to one event type. Omit to clear the key on any event type in the workspace that defines it.
subject *objectWho the clear is about. At least one field is required.
externalRef stringThe identifier your system matched on. Defaults to the subject’s reference.
evidenceUrl stringA link to the underlying record.
note stringA line for the host. Never shown to the attendee.

Responses

StatusBodyDescription
200Preclearance

A replay of the same Idempotency-Key: the first response, verbatim.

201Preclearance

The pre-clearance is on file and waiting for a booking.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No event type with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

GET /api/v1/event-types/{id}/requirements

Read an event type’s requirements

Requires the requirements:read scope.

The prerequisites this event type asks for. Each one is copied onto every booking made on it, so editing the wording here changes what future attendees are asked and leaves what past attendees agreed to exactly as it was.

Host approval is not listed: it is the event type’s own "requires confirmation" setting, and giving it a second home here is how the two would come to disagree.

Parameters

NameInTypeDescription
id *pathstringEvent type id.

Responses

StatusBodyDescription
200EventTypeRequirementList

The event type’s requirement definitions.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No event type with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

PUT /api/v1/event-types/{id}/requirements

Set an event type’s requirements

Requires the requirements:write scope.

Replaces the whole list. A definition that is absent from the array is deactivated rather than deleted — bookings already held on it keep their own copy of the wording, and deleting the template would strand them.

An empty array clears them. host_approval is refused as a key. allowAttendeeEvidence is only valid on an external requirement, and an attendee_acknowledgement needs the ackText the attendee is agreeing to.

Parameters

NameInTypeDescription
id *pathstringEvent type id.

Request body (required)

FieldTypeDescription
requirements *array of EventTypeRequirementThe complete list, in the order attendees should read it.

Responses

StatusBodyDescription
200EventTypeRequirementList

The event type’s requirements as they now stand.

400Error

The request body is not valid.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No event type with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

Questions

Questions an event type asks, and the answers a booking carries. A question is asked at booking or after it; an after-booking question is answered by the attendee on their own booking page and never holds anything up.

GET /api/v1/event-types/{id}/questions

List an event type’s questions

Requires the event_types:read scope.

Both stages by default, in the order they are shown. Pass ?stage=after_booking to list only the questions the attendee answers on their own booking page, or ?stage=at_booking for the booking form's.

Parameters

NameInTypeDescription
id *pathstringEvent type id.
stage query"at_booking" | "after_booking"

Responses

StatusBodyDescription
200QuestionList

The event type’s questions.

400Error

stage is not one of the two values.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No event type with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

POST /api/v1/event-types/{id}/questions

Add a question to an event type

Requires the event_types:write scope.

stage defaults to at_booking, which is the booking form. Set it to after_booking and the question never appears on the form; the attendee answers it on their booking page, whenever they are ready, and isRequired then means one reminder rather than a gate.

A condition may only point backwards in stage: a booking-form question cannot depend on an after-booking answer, because that answer does not exist when the form is rendered.

Parameters

NameInTypeDescription
id *pathstringEvent type id.

Request body (required)

FieldTypeDescription

Responses

StatusBodyDescription
201

The question that was created.

400Error

invalid_body, or condition_stage_order when a condition would point forwards in stage.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No event type with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

PATCH /api/v1/event-types/{id}/questions/{questionId}

Update a question

Requires the event_types:write scope.

Any subset of the create body. Moving a question to after_booking while a booking-form question depends on it is refused with condition_stage_order, which is the same rule read from the other end. Conditions name option TEXT: renaming an option rewrites every rule that named it, and removing an option another question's condition still names is refused with 422 unless the body sets detachConditions to true.

Parameters

NameInTypeDescription
id *pathstringEvent type id.
questionId *pathstringQuestion id.

Request body (required)

FieldTypeDescription

Responses

StatusBodyDescription
200

The updated question.

400Error

invalid_body, or condition_stage_order.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such question on an event type belonging to the authenticated user.

422Error

condition_option_in_use or condition_question_in_use: another question's condition depends on the question being changed, or on an option this write would remove. Nothing is written. Repeat the request with detachConditions to drop those rules and apply the write anyway.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

DELETE /api/v1/event-types/{id}/questions/{questionId}

Remove a question

Requires the event_types:write scope.

A soft delete: the question stops being asked, and every answer already given to it stays readable on the bookings that carry it. A question that another question's condition depends on is refused with 422; pass ?detachConditions=true to drop those rules and delete it anyway.

Parameters

NameInTypeDescription
id *pathstringEvent type id.
questionId *pathstringQuestion id.
detachConditions querybooleanSet to true to drop the conditions that depend on this question instead of refusing with 422.

Responses

StatusBodyDescription
200

The question is no longer asked.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No such question on an event type belonging to the authenticated user.

422Error

condition_option_in_use or condition_question_in_use: another question's condition depends on the question being changed, or on an option this write would remove. Nothing is written. Repeat the request with detachConditions to drop those rules and apply the write anyway.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

GET /api/v1/bookings/{id}/responses

Read a booking’s answers

Requires the bookings:read scope.

Every question the event type asks, answered or not, in the order they are shown. A question with answer: null has not been answered; answeredAt is null for an answer given on the booking form.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Responses

StatusBodyDescription
200BookingResponseList

The booking’s answers.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

No booking with that id belongs to the authenticated user.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

PUT /api/v1/bookings/{id}/responses

Write a booking’s answers

Requires the bookings:write scope.

A merge: only the questions named in the body change, and null clears an answer. Values are validated against each question's type and options exactly as the booking form validates them. Any stage may be written — correcting a typo in a booking-form answer is legitimate — and the write is recorded as answered by api.

Idempotent by construction, so no Idempotency-Key is needed: sending the same body twice leaves the same answers and fires one booking.responses_updated, because the second save changes nothing.

Parameters

NameInTypeDescription
id *pathstringBooking id.

Request body (required)

FieldTypeDescription
responses *array of object

Responses

StatusBodyDescription
200BookingResponseList

The full answer set after the merge.

400Error

invalid_response: a value does not fit its question.

401Error

No credential was presented, or it did not resolve.

403Error

The credential resolved but lacks the scope this operation requires. The message names the missing scope.

404Error

question_not_found, or no such booking.

409Error

booking_not_editable: the booking is canceled or rejected.

429RateLimitError

More than 60 requests in the current 60-second window. Note the body shape differs from every other error.

500Error

The workspace's database or cache binding was not available to serve the request.

Webhook events

These are the events Calemander sends to you. Subscribe with POST /api/v1/webhooks; the signing secret is returned once, on creation.

booking.canceled

A booking was cancelled

Fires when a booking is cancelled by the invitee, by the host, as part of cancelling a recurring series, when a pending booking is rejected, or through PATCH /api/v1/bookings/{id}. canceledBy and cancellationReason are populated where they were recorded.

booking.checked_in

An attendee was checked in

Fires when a confirmed booking is checked in to its session — by staff on the roster, by the attendee at the door or from their phone, or by a walk-in booking. The booking keeps status: confirmed; the payload adds checkedInAt and checkedInBy (staff, self, door or auto). Undoing a check-in sends nothing.

booking.confirmed

A pending booking was approved

Fires when a booking that required confirmation is approved by the host, when payment completes for a paid event type, or when PATCH /api/v1/bookings/{id} moves a booking to confirmed.

Calendly-format subscribers never receive this event — Calendly has no approval step and therefore no equivalent name, so nothing is sent rather than something the receiver cannot dispatch.

booking.created

A booking was made

Fires when a booking is created through the public booking flow, including recurring series. It does not fire for bookings created through POST /api/v1/bookings.

booking.held

A booking is held on unmet requirements

Fires when a booking takes a time but cannot be confirmed yet: its seat is out of availability and one or more of its requirements is neither cleared nor waived. The payload carries the requirement list.

There is no calendar event and no confirmation email at this point, and the attendee has been told so. booking.confirmed follows when the last requirement clears.

Calendly-format subscribers never receive this event — Calendly has no held booking and therefore no equivalent name, so nothing is sent rather than something the receiver cannot dispatch.

booking.no_show

An invitee was marked as a no-show

booking.no_show_cleared

A no-show mark was taken back

Fires when a booking leaves the no_show status. When the change also produces another event — a no-show booking moved straight to canceled, say — both are delivered.

booking.rescheduled

A booking moved to a new time

Fires when a booking is rescheduled. The payload carries the new startTime/endTime and the previous ones as oldStartTime/oldEndTime; status is confirmed.

On the calendly format this single event becomes two deliveries, matching what Calendly itself sends: an invitee.canceled for the old time with rescheduled: true, followed by an invitee.created for the new time carrying old_invitee.

{
  "event": "booking.rescheduled",
  "payload": {
    "eventTypeId": "et_intro30",
    "eventTypeSlug": "intro-30",
    "eventTypeName": "Intro call",
    "durationMinutes": 30,
    "locationType": "google_meet",
    "meetingUrl": "https://meet.google.com/abc-defg-hij",
    "attendeeNotes": null,
    "attendeeTimezone": "Europe/London",
    "host": {
      "name": "Ada Lovelace",
      "email": "ada@example.com"
    },
    "answers": [],
    "bookingCreatedAt": "2026-08-20T11:02:00Z",
    "canceledBy": null,
    "cancellationReason": null,
    "paymentStatus": null,
    "guestEmails": [],
    "joinUrl": "https://ada.calemander.com/join/bkg_4c21/eyJ2IjoxLCJ0Ijo...",
    "oneOffLinkId": null,
    "oneOffToken": null,
    "bookingId": "bkg_4c21",
    "eventSlug": "intro-30",
    "eventName": "Intro call",
    "startTime": "2026-09-15T15:00:00Z",
    "endTime": "2026-09-15T15:30:00Z",
    "oldStartTime": "2026-09-14T15:00:00Z",
    "oldEndTime": "2026-09-14T15:30:00Z",
    "attendeeName": "Grace Hopper",
    "attendeeEmail": "grace@example.com",
    "status": "confirmed"
  },
  "timestamp": "2026-08-29T09:14:22.115Z"
}

requirement.cleared

A requirement was satisfied

Fires when one requirement moves to cleared or waived. The payload carries the full provenance — clearMethod, clearedBy, verifiedBy, referenceMatched, externalRef and evidenceRef — so a subscriber can hold a weakly-cleared booking to a different standard than a strongly-cleared one without a second API call.

It fires once per actual move. A replay, an already-cleared requirement and a repeated reference all change nothing and send nothing. When the clear was the last one, booking.confirmed follows.

Calendly-format subscribers never receive this event.

requirement.submitted

An attendee supplied a reference

Fires when an attendee submits evidence for a requirement — the county application number they are holding, say. The requirement moves to submitted and the booking stays held: this is a claim about the outside world, not a fact about it.

This is the event an integration subscribes to in order to close the loop by itself. Your system hears the reference the moment the attendee types it, can go and match on it, and can then clear the requirement — at which point the two independent sources agree and the record says so.

Calendly-format subscribers never receive this event.

booking.released

A held booking was released

Fires when a booking held on outstanding requirements reaches its release time with something still uncleared. The booking becomes status: canceled with cancellation_reason naming what was outstanding, and the seat goes back on sale. The payload adds outstanding (the labels still unmet), releasedAt, rebookUrl, and refundIssued, which is always false — releasing a hold never refunds, and whether money is returned is the host's decision rather than this event's. A requirement that clears after this fires is recorded on the released booking and does not un-cancel it.

booking.responses_updated

An after-booking answer changed

Fires whenever an answer to a question the event type asks after booking actually changes — from the attendee's own booking page or from PUT /bookings/{id}/responses. Never fires for a save that changed nothing. The payload adds changedQuestionIds (the ids that moved), answeredBy (attendee, host or api), responses (the full set for the booking, both stages, each with stage and answeredAt), and detailsUrl. There is no Calendly-compatible name for this event, so compat subscribers do not receive it. A required after-booking question never changes a booking's status, so this event never coincides with one.

routing_form_submission.created

A routing form was submitted

Fires when someone submits a routing form. Submissions are not stored, so submissionId identifies this delivery rather than a row you can fetch later. This payload receives none of the booking enrichment fields.

standby.offered

A freed seat was offered to someone on standby

Fires when a seat comes free on a full class and the person at the front of the standby line is offered it. One person per seat: nobody else is offered the same seat while this offer is open. offerExpiresAt is when the hold ends, which is the event type's standbyOfferMinutes or five minutes when the class starts within 30. An offer that runs out sends nothing; the entry goes back to the end of the line.

standby.promoted

Someone on standby got the seat

Fires when a standby entry becomes a booking — they accepted an offer, the host promoted them from the roster, or a close-out gave them a seat freed by a no-show. bookingId is the booking that now exists; the ordinary booking.created fires for it as well.

Schemas

Booking

FieldTypeDescription
id *string
eventTypeId *string
eventTypeName *string or nullNull if the event type row is gone.
eventTypeSlug *string or null
startTime *stringUTC, as stored.
endTime *stringUTC, as stored.
attendeeName *string
attendeeEmail *string
attendeeNotes *string or null
attendeeTimezone *string or nullIANA timezone the attendee booked in, e.g. Europe/London. Null on rows recorded before the column existed.
status *stringValues in use: confirmed, pending, canceled, rescheduled, payment_pending, no_show.
meetingUrl *string or null
paymentStatus *string or nullNull for free event types.
guestEmails *array of stringAdditional invitees. Empty when there are none.
createdAt *string
detailsUrl string or nullThe attendee's booking page, when the event type asks anything after booking. Null otherwise. Put it in your own receipt when you have turned Calemander's held email off.
responsesUpdatedAt string or nullWhen an after-booking answer on this booking last changed. Null when every answer came in with the booking.

BookingRequirement

One prerequisite on one booking, with the full record of how it reached its state.

FieldTypeDescription
key stringThe host-authored, stable identifier for this requirement.
kind "host_approval" | "external" | "attendee_acknowledgement"Who can satisfy it: a workspace member, an outside system or verified attendee evidence, or the attendee ticking a statement.
label stringThe attendee-visible name, snapshotted when the booking was made.
instructions string or nullAttendee-visible body text, snapshotted.
instructionsUrl string or nullWhere the attendee goes to do the thing.
state "outstanding" | "submitted" | "cleared" | "waived" | "failed"Only cleared and waived satisfy. submitted is the attendee’s claim and satisfies nothing; failed is a rejected claim and holds the booking.
evidenceRef string or nullWhat the attendee typed, when they were allowed to supply a reference.
externalRef string or nullThe clearing system’s own identifier for what it matched.
clearedAt string or null
clearedBy "host" | "attendee" | "api" | "system" | nullWhich kind of credential cleared it. Set by the server, never by the request.
clearMethod "system_reference" | "host_manual" | "attendee_evidence" | nullHow it was cleared. Derived from the row’s state and the credential; there is no request field for it.
verifiedBy "api" | "host" | nullOn an attendee_evidence clear, which kind of verifier said yes.
referenceMatched booleanTrue when the clearing system’s reference is the one the attendee had already typed — two independent sources naming the same file.
note string or nullWhy it was waived or sent back. Written for the host and never shown to the attendee.
sortOrder integer

BookingRequirementList

FieldTypeDescription
bookingId string
outstanding integerHow many requirements are still neither cleared nor waived.
requirements array of BookingRequirement

BookingResponse

FieldTypeDescription
questionId string
question stringThe question's label.
answer string or boolean or array of string or nullThe stored answer, or null when the question has not been answered.
stage "at_booking" | "after_booking"
answeredAt string or nullNull for an answer given on the booking form.

BookingResponseList

FieldTypeDescription
bookingId string
responses array of BookingResponse
detailsUrl string or nullThe attendee's booking page, when the event type asks anything after booking.

BookingWebhookEnvelope

The calemander wire format: one message per event. Signed with HMAC-SHA256 over the raw body, hex-encoded, in X-Webhook-Signature; X-Webhook-Timestamp echoes the timestamp below. Subscribers on the calendly format receive Calendly's own envelope and a Calendly-Webhook-Signature: t=<unix>,v1=<hex> header instead, where v1 is HMAC-SHA256 over <t>.<raw body>.

FieldTypeDescription
event *WebhookEventName
payload *BookingWebhookPayload
timestamp *stringWhen the event fired. Shared by every message one event produces.

BookingWebhookPayload

The eight base fields are always present. The rest are enrichment, loaded from the booking at fire time — if that lookup fails the message still ships, carrying the base fields alone. Treat every enrichment field as optional.

FieldTypeDescription
bookingId *string
eventSlug *string
eventName *string
startTime *stringUTC.
endTime *stringUTC.
attendeeName *string
attendeeEmail *string
status *string
oldStartTime stringThe time the booking moved from. Present on booking.rescheduled only.
oldEndTime stringPresent on booking.rescheduled only.
eventTypeId string
eventTypeSlug string or null
eventTypeName string or null
durationMinutes integer
locationType string or null
meetingUrl string or null
attendeeNotes string or null
attendeeTimezone string or nullIANA timezone the attendee booked in, e.g. Europe/London.
host null or objectThe host assigned to this booking — the round-robin member where one is set, the event owner otherwise. Null when no host email could be resolved.
answers array of objectThe invitee's answers to the event type's booking questions, in question order.
bookingCreatedAt string
canceledBy string or null
cancellationReason string or null
paymentStatus string or null
guestEmails array of string
joinUrl string or nullThe attendee-facing join link, <origin>/join/<bookingId>/<token> — the same link the confirmation email and the .ics carry. Show this one to people. NOT the same as meetingUrl, which is the raw provider URL: a reschedule re-mints the provider event, so meetingUrl goes stale in every copy already sent, while this link resolves the provider URL at click time and survives. Null when the booking is canceled, and when it is a phone or in-person appointment that never has a meeting to join. NOT null for a video booking whose provider event has not been created yet — that page self-refreshes while the meeting is prepared.
oneOffLinkId string or nullThe one-off scheduling link this booking was made through, as the stable id for GET /api/v1/one-off-links. Null when the booking came from the public booking page.
oneOffToken string or nullThe token from that one-off link's own URL, and the field to match on: whatever minted the link already holds this string, so it identifies the order, lead or case that issued the invitation with no email or name heuristic. Null when the booking came from the public booking page. one_off_links.booking_id holds only the latest booking through a link, so a multi-use link reports its token on its most recent booking and null on earlier ones.

Contact

FieldTypeDescription
id *string
name *string
email *string
phone *string or null
company *string or null
tags *array of string
source *stringHow the contact was first recorded.
bookingCount *integer
lastBookingAt *string or null
createdAt *string

Error

The error shape used by every endpoint except the 429.

FieldTypeDescription
message *stringHuman-readable explanation.

EventType

FieldTypeDescription
id *string
name *string
slug *stringUsed in booking URLs and in the availability path.
duration *integerMeeting length in minutes.
maxDaysAhead integer or nullHow many days ahead this event type may be booked; null means no limit. Counted as calendar dates on the HOST's own timezone, so 30 means every date up to and including the one 30 days after the host's today. Dates past the window are not offered on the booking page, and a booking for one is refused with 409.
description *string or null
isActive *boolean
coverImage *string or null
inviteCalendar *string or nullThe raw stored legacy setting. Prefer locationType and calendarProvider, which are the resolved values.
locationType *string or nullWhere the meeting happens, resolved against the owner's defaults.
calendarProvider *string or nullWhich calendar the event and the attendee invite land on, resolved against the owner's defaults.
meetingMode *"one_on_one" | "round_robin" | "collective"Defaults to one_on_one when unset.
format *"one_on_one" | "group_class" | "round_robin" | "collective"The event type's shape as one value, and the field to read in preference to meetingMode + maxSeats. group_class is the only format with seats: it is what meetingMode one_on_one and maxSeats above 1 mean together. Event types written before this field existed report the format derived from that pair, so it is never absent.
hosts *array of EventTypeHostActive member hosts. Empty for one_on_one event types.
maxSeats *integer or nullSeats per slot for group events; null for one-attendee events.
capacity *integer or nullSeats on a group class — the same number as maxSeats, named for what it means. Null for every format other than group_class.
color *string or nullThe event type's #rrggbb accent colour.
autoCloseMinutes *integer or nullFor a group event type: minutes after start at which check-in closes on its own; null = the door is closed by hand. Always null for a one-on-one.
creditTiming *"booking" | "attendance"When a package credit is spent: at booking, or at check-in (attendance). Always booking for a one-on-one.
standbyEnabled booleanWhether a full class offers a place in line instead of a dead end.
standbyOfferMinutes integerHow long one person holds a freed seat before it passes to the next.
redirectUrl *string or nullWhere an invitee is sent after booking, if configured.
redirectPassDetails *booleanWhether booking details are appended to redirectUrl as query parameters.
sendHeldNotifications *booleanWhether Calemander emails and texts the attendee when their time is held. Defaults to true. Set it to false when your own site tells the customer at the moment of payment, so they are not told the same thing twice by two senders. It silences only that one message: the host's own notification, the booking.held and booking.created webhooks, the release clock, the reminder that a hold is running out, the notice that a hold was released, and the confirmation email all still go, and a resend requested by a person still sends the held email.
priceCents *integer or nullPrice in minor units; null for free event types.
currency *stringDefaults to usd.
createdAt *string
priceDisplay "auto" | "hidden"Whether the booking page shows a price row. 'auto' derives it from the price and deposit, showing 'Free' when nothing is charged. 'hidden' shows no price row anywhere — on the booking page, the embed, the sidebar or the confirmation screen — for a host who collects payment on their own site, where 'Free' would be untrue. It changes only what is displayed; nothing about what is charged, refunded or held changes.
policyLineOverride string or nullThe one-line cancellation policy shown under the price, in the host's own words, used verbatim. Null (the default) generates the sentence from the cancellation settings. Not applied to a card-on-file fee sentence, which discloses an amount that can be charged and must keep matching the booking row.
submitLabel string or nullThe submit button's text, used verbatim. Null (the default) derives it from what the event type is called to its attendee: 'Book meeting', 'Book appointment' or 'Book class'. Applied only when the click leads to no payment step; a button that continues to payment, saves a card or spends a package credit keeps its generated label so it cannot be relabelled to hide a charge.

EventTypeHost

FieldTypeDescription
userId *string
name *string
email *string
weight *numberThis host's target share of round-robin assignments.

EventTypeRequirement

A requirement as defined on an event type — the template copied onto every booking.

FieldTypeDescription
key string
kind "host_approval" | "external" | "attendee_acknowledgement"
label string
instructions string or null
instructionsUrl string or null
ackText string or null
allowAttendeeEvidence boolean
evidenceLabel string or null
evidenceHint string or null
preclearTtlDays integer
preclearReusable boolean
gate "hold" | "confirm"Which moment this requirement stands at. confirm (the default) is satisfied after the booking exists: the seat is held and clearing the last requirement confirms it. hold means no time may be taken until it is met — the public booking page refuses the booking outright rather than letting someone hold a seat on a prerequisite they cannot satisfy. Not valid on an attendee_acknowledgement, which is ticked on the booking’s own page and so cannot precede it.
sortOrder integer

EventTypeRequirementList

FieldTypeDescription
eventTypeId string
requirements array of EventTypeRequirement

EventTypeWrite

Body of POST /api/v1/event-types (where name, slug and durationMinutes are required) and PATCH /api/v1/event-types/{id} (where every field is optional and only the fields present are changed). Unknown fields are refused with 400.

These are the fields the dashboard editor writes that the API exposes. Payment settings (mode, price, deposit, fees), buffers, booking limits, questions and redirects are configured in the dashboard.

FieldTypeDescription
name string
slug stringLowercase letters, digits and hyphens; unique across the workspace. A taken slug is 400 on create and 409 on patch.
description string or null
durationMinutes integer
maxDaysAhead integer or nullHow many days ahead this event type may be booked, 1-365. Send null to remove the limit, which is what an event type means until one is set. Counted as calendar dates on the HOST's own timezone. Dates past the window are not offered and cannot be booked.
maxSeats integer or nullAbove 1 makes a group event type; null or 1 is a one-on-one (stored as null). Turning a group into a one-on-one clears autoCloseMinutes and resets creditTiming.
format "one_on_one" | "group_class" | "round_robin" | "collective"The event type's shape, as one field. Prefer it to maxSeats and meetingMode, which it sets: group_class stores capacity seats, and every other format stores one seat. Sending it alongside either of those is 400 — a body naming both axes has no single right answer. Switching away from group_class clears the seats, autoCloseMinutes and creditTiming, and switching to round_robin or collective carries the same admin and memberIds rules as meetingMode.
capacity integerSeats on a group class. Only accepted with format: "group_class" (400 otherwise). Omitted on PATCH, a class keeps the seats it already had; omitted on create it starts at 12.
locationType "google_meet" | "zoom" | "teams" | "jitsi" | "phone" | "in_person" | nullSet together with calendarProvider to override the workspace default, or null on both to inherit it.
calendarProvider "google" | "outlook" | "none" | null
meetingMode "one_on_one" | "round_robin" | "collective"Changing to or from a team mode requires the credential's owner to be a workspace admin (403 otherwise); a team mode needs memberIds.
memberIds array of stringHost user ids for a team mode. Every id must be a member of this workspace with a connected calendar; ignored for one_on_one. On PATCH, members left out are removed and hosts that stay keep their round-robin weight.
color string or null
isActive booleanDefaults to true on create. Switching an event type ON counts against the plan's active-event-type cap (402 when full); on create the event type is created switched off instead and the response says so.
autoCloseMinutes integer or nullGroup event types only. Turning auto-close on requires a plan with check-in (402 otherwise).
creditTiming "booking" | "attendance"Group event types only. Switching to attendance requires a plan with check-in (402 otherwise).
standbyEnabled booleanGroup event types only. When the class is full the booking page offers a place in line, and a freed seat is offered to one person at a time, so it can never be given away twice. Turning standby on requires a plan with check-in (402 otherwise); turning it off is never gated, and an existing line keeps working whatever the plan says.
standbyOfferMinutes integerHow long one person holds a freed seat before it passes to the next, 5-1440. Within 30 minutes of the start the hold is 5 minutes instead, and a hold never runs past the start time. Default 120.
sendHeldNotifications booleanDefaults to true. Set it to false when your own site tells the customer at the moment of payment that their time is reserved, so Calemander does not say the same thing beside it. It silences only the attendee's held email and held text on automatic sends: the host's notification, the booking.held and booking.created webhooks, the release clock, the nudge, the release notice and the confirmation are unchanged, and a resend requested by a person still sends the held email.
priceDisplay "auto" | "hidden"Whether the booking page shows a price row. 'auto' derives it from the price and deposit, showing 'Free' when nothing is charged. 'hidden' shows no price row anywhere — on the booking page, the embed, the sidebar or the confirmation screen — for a host who collects payment on their own site, where 'Free' would be untrue. It changes only what is displayed; nothing about what is charged, refunded or held changes.
policyLineOverride string or nullThe one-line cancellation policy shown under the price, in the host's own words, used verbatim. Null (the default) generates the sentence from the cancellation settings. Not applied to a card-on-file fee sentence, which discloses an amount that can be charged and must keep matching the booking row.
submitLabel string or nullThe submit button's text, used verbatim. Null (the default) derives it from what the event type is called to its attendee: 'Book meeting', 'Book appointment' or 'Book class'. Applied only when the click leads to no payment step; a button that continues to payment, saves a card or spends a package credit keeps its generated label so it cannot be relabelled to hide a charge.

Identity

FieldTypeDescription
user *object
workspace *object
auth *objectWhich credential type authenticated this request.
api_key *null or objectDescribes the API key that authenticated. Null for an OAuth caller, which has no key.

NoShowResult

The outcome of marking or clearing a no-show, including the status the booking now carries.

FieldTypeDescription
success *true
id *stringThe booking id.
status *"no_show" | "confirmed"The booking’s status after the call.
FieldTypeDescription
id *string
label *string or null
event_type_id *string
event_type_name *string
event_type_slug *string
url *stringThe bookable link, built on the host you called. The raw token is not returned separately.
expires_at *string or null
max_uses *integer
use_count *integerBookings made through this link so far.
booking_id *string or nullThe booking this link produced, where one is recorded.
is_expired *booleanComputed at read time: expires_at is set and in the past.
is_used *booleanComputed at read time: use_count has reached max_uses.
created_at *string

PageNumber

The page that was served.

Type: integer

Preclearance

FieldTypeDescription
preclearanceId string
key string
eventTypeId string or null
subject object
externalRef string or null
reusable boolean
expiresAt string
consumedAt string or null
consumedBookingId string or null

Question

One question on an event type.

FieldTypeDescription
id string
eventTypeId string
label string
description string or null
placeholder string or null
questionType "text" | "textarea" | "email" | "phone" | "yes_no" | "dropdown" | "radio" | "checkbox"
options array or null
allowOther boolean
suggestions array or null
isRequired booleanOn an after-booking question this is a reminder, never a gate: the booking is held or confirmed exactly as it would be without an answer.
minLength integer or null
maxLength integer or null
conditionQuestionId string or null
conditionOperator "equals" | "not_equals" | "contains" | "is_empty" | "is_not_empty" | null
conditionValue string or null
stage "at_booking" | "after_booking"When the question is asked. at_booking is shown on the booking form; after_booking never is, and is answered on the attendee's own booking page.
sortOrder integer

QuestionInput

FieldTypeDescription
label *string
description string or null
placeholder string or null
questionType *"text" | "textarea" | "email" | "phone" | "yes_no" | "dropdown" | "radio" | "checkbox"
options array or null
allowOther boolean
suggestions array or null
isRequired booleanOn an after-booking question this is a reminder, never a gate: the booking is held or confirmed exactly as it would be without an answer.
minLength integer or null
maxLength integer or null
conditionQuestionId string or null
conditionOperator "equals" | "not_equals" | "contains" | "is_empty" | "is_not_empty" | null
conditionValue string or null
stage "at_booking" | "after_booking"When the question is asked. at_booking is shown on the booking form; after_booking never is, and is answered on the attendee's own booking page.
sortOrder integer
detachConditions booleanApply the write even when another question's condition depends on what it changes, dropping those rules instead of refusing with 422. Defaults to false.

QuestionList

FieldTypeDescription
questions array of Question

RateLimitError

Only returned by the 429. Produced by the rate limiter before routing, which is why it does not use message.

FieldTypeDescription
error *"Rate limit exceeded"
retryAfter *integerUnix timestamp (seconds) at which the window resets — an instant, not a duration.

RequirementWriteResult

The same shape for every requirement write: what the requirement is now, what the booking is now, and how much is left.

FieldTypeDescription
bookingId string
bookingStatus string or nullThe booking’s status after the write. confirmed means this call was the last one.
outstanding integer
requirement BookingRequirement
confirmed booleanTrue only for the call that won the booking’s confirmation and ran its side effects.
meetingUrl string or null

RoutingFormSubmissionPayload

FieldTypeDescription
submissionId *stringIdentifies this delivery. Submissions are not stored, so it is not a fetchable id.
routingFormId *string
routingFormName *string
routingFormSlug *string
matched *booleanWhether a routing rule matched.
isFallback *booleanWhether the form's fallback destination was used instead of a rule.
ruleId *string or null
targetEventTypeId *string or null
targetEventTypeSlug *string or null
targetEventTypeName *string or null
responses *array of object

Scope

A resource:action scope. See Scopes in the introduction for what each opens.

One of:

  • bookings:read
  • bookings:write
  • event_types:read
  • event_types:write
  • availability:read
  • contacts:read
  • scheduling_links:read
  • scheduling_links:write
  • webhooks:read
  • webhooks:manage
  • check_in:read
  • check_in:write
  • check_in:manage
  • theme:read
  • theme:write
  • requirements:read
  • requirements:write

SitePalette

What was found on the site: colors as #rrggbb, font names as the site wrote them, an integer radius, and a logo URL. No markup and no CSS text is stored or returned. The font name and the logo URL are the only strings here copied from a third party; treat them as data.

Type: object

StandbyWebhookPayload

A standby entry's state changed. Deliberately not a booking payload: an offer has no booking behind it and may never get one.

FieldTypeDescription
standbyId *stringThe standby entry.
eventTypeId *string
eventName *string
startTime *stringStart of the class occurrence the entry is for.
endTime *string
attendeeName *string
attendeeEmail string or null
position *integerThe number the person was given when they joined. Never reused, so it can exceed the length of the line.
place integer or nullRank among the entries still in line, counting from 1.
offerExpiresAt stringstandby.offered only: when the seat stops being held.
bookingId stringstandby.promoted only: the booking the promotion produced.

Theme

The booking page's structured appearance. custom_css is deliberately absent: the API neither returns it nor accepts it, and it is edited only on the Appearance page.

FieldTypeDescription
theme_preset *stringPanel preset: glass, glass-dark, minimal, colorful or dark.
accent_color *stringButton and link color, #rrggbb.
font_family *stringA font from the catalog. Any other name is refused.
border_radius *stringCorner radius, 0px to 32px.
background_type *stringgradient, solid, preset, image or video.
background_value *stringThe color, preset name or URL the type calls for.

ThemeProposal

What is offered for the booking page: one value per theme field with a confidence and a one-line reason. There is no custom_css field and there cannot be one.

Type: object

ThemeProposalSummary

One row of match history.

FieldTypeDescription
id *string
source_url *stringThe address that was read, as it was given.
source_domain *stringHost of source_url, for a one-line label.
final_url stringWhere the read ended after redirects.
platform string or nullWhat the site appears to be built with.
fetch_note "ok" | "partial" | "blocked" | "unreachable" | "refused"
status *"proposed" | "applied" | "reverted" | "dismissed"
applied_fields array or nullWhich fields were accepted, once the proposal was applied.
created_at *string
applied_at string or null

TimeSlot

FieldTypeDescription
start *stringUTC.
end *stringUTC.
best truePresent only on slots the ranking picked out as preferable. Absent, not false, on ordinary slots — so a payload with no ranking is indistinguishable from one where nothing ranked.

Total

Rows matching the filter, across all pages.

Type: integer

TotalPages

Pages available at this limit.

Type: integer

WebhookEventName

One of:

  • booking.created
  • booking.canceled
  • booking.rescheduled
  • booking.confirmed
  • booking.no_show
  • booking.no_show_cleared
  • booking.checked_in
  • booking.held
  • requirement.submitted
  • requirement.cleared
  • booking.released
  • booking.responses_updated
  • routing_form_submission.created
  • standby.offered
  • standby.promoted

WebhookSubscription

A subscription as returned by the list and create endpoints.

FieldTypeDescription
id *string
url *string
events *array of WebhookEventName
scope *"user" | "organization"
payload_format *"calemander" | "calendly"
is_active *booleanFalse once the endpoint has answered 410 Gone, or after it was disabled.
created_at *string
user_id stringThe subscription's owner. Returned by the list endpoint; absent from the create response.

WebhookSubscriptionDetail

A subscription as returned by GET /api/v1/webhooks/{id}. Note the absence of payload_format.

FieldTypeDescription
id *string
url *string
events *array of WebhookEventName
scope *"user" | "organization"
is_active *boolean
created_at *string
user_id *string

Still stuck? Email support@calemander.com · Privacy · Terms