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
A credential resolves to exactly one user in one workspace, and how far it reaches inside that workspace follows that user's role, exactly as the dashboard does: a credential whose owner is a workspace admin reads and writes every host's bookings in the workspace, and one whose owner is a member only the bookings that member hosts. GET /api/v1/me reports which of the two you hold as reach. Everything that is not a booking — event types, contacts, one-off links, availability, themes — stays filtered to the credential's own rows whatever the role. The other deliberate widening is webhook subscriptions: a workspace admin also sees and manages organization-scoped subscriptions created by other members.
Reach is read from the owner's live role on every request, so promoting or demoting someone changes what their existing keys and grants do immediately, and there is no scope to tick and no key to re-create. Booking rows carry hostId and hostEmail so a workspace-wide list says whose each booking is, and GET /bookings?host= narrows one to a single host. A booking outside a credential's reach answers 404, never 403, so a member credential cannot use the API to learn which booking ids exist.
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 key —
cm_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 token —
cmo_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 — the bookings within your reach (see above), with each attendee's name, email and notes | | bookings:write | POST /bookings (always on your own event types, as yourself), PATCH /bookings/{id}, POST/DELETE /bookings/{id}/no-show — the writes reach whatever your role reaches | | 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. A refusal that has a machine-readable reason also carries code next to message (slot_taken, claim_expired, record_payment_instead and so on); branch on code, not on the wording of message. A few 429s come from elsewhere and are described under the RateLimited response: an OAuth client over its own budget, a workspace minting too many checkout tokens, and the theme endpoints' site-read budgets.
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
| Status | Body | Description |
|---|---|---|
| 200 | Identity | The credential's identity. |
| 401 | Error | The credential is missing, invalid, or no longer resolves to a user. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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"
},
"reach": "workspace",
"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
| Name | In | Type | Description |
|---|---|---|---|
| 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 | query | integer | 1-based page number. Values below 1 and unparseable values fall back to 1. |
| limit | query | integer | Rows per page. Clamped to 1–100; unparseable values fall back to 50. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | A page of event types. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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)
| Field | Type | Description |
|---|
{
"name": "Morning Flow",
"slug": "morning-flow",
"durationMinutes": 60,
"maxSeats": 12
} Responses
| Status | Body | Description |
|---|---|---|
| 201 | — | Created. |
| 400 | Error | 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. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 402 | Error |
|
| 403 | Error | The credential lacks the |
| 422 | Error | Every value is well-formed but the combination is refused: the hold clocks do not fit inside each other, or |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | The event type id. |
Request body (required)
| Field | Type | Description |
|---|
{
"name": "Evening Flow",
"isActive": true
} Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Updated. |
| 400 | Error | 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, |
| 401 | Error | No credential was presented, or it did not resolve. |
| 402 | Error | Switching the event type on would exceed the plan's active-event-type cap, or auto-close, attendance credits or standby were turned on on a plan without check-in. |
| 403 | Error | The credential lacks the |
| 404 | Error | No event type with this id is owned by the authenticated user. |
| 409 | Error | The new slug is already used by another event type in the workspace. |
| 422 | Error | Every value is well-formed but the combination is refused: the hold clocks do not fit inside each other, or |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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 bookings within this credential's reach, most recent start time first: every host's in the workspace for an admin credential, the credential owner's own for a member. All filters combine with AND.
A booking of an event type that sells nights or days carries its stay (dates, inventory, units, charges); every other booking carries stay: null.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| status | query | string | Exact 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 | query | string | Only bookings for this event type. |
| from | query | string | Lower bound on startTime, inclusive. Compared as a string against the stored UTC value, so pass the same ISO 8601 shape the API returns. |
| to | query | string | Upper bound on startTime, inclusive. Same string-comparison note as from. |
| payment_provider | query | string | Exact 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 | query | string | Exact match on the attendee's email, case-insensitively. Not a substring search: a@b.com will not match aa@b.com. |
| host | query | string | Only bookings hosted by this user id, as reported in each row's hostId. Meant for narrowing a workspace-wide list to one host. A member credential asking for a host other than itself gets an empty list, not someone else's bookings. |
| page | query | integer | 1-based page number. Values below 1 and unparseable values fall back to 1. |
| limit | query | integer | Rows per page. Clamped to 1–100; unparseable values fall back to 50. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | A page of bookings. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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. Creating is not reaching: this always books on the credential's own event type and hosts the booking as the credential's own user, whatever its role.
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, or schedule reminders. Those belong to the public booking flow. It does fire webhooks, as every other create path does: booking.created, then booking.held when the booking parks on a requirement. 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.
Stays. For an event type that sells nights or days, send stay instead of times. The booking is created confirmed (or pending when the event type requires confirmation or a requirement holds it; status may say pending, and any other status is refused with 400 stay_status_invalid), its startTime/endTime are the check-in and check-out instants, and the 201 body carries its stay. Sending times for such an event type is 400 stay_event_type; sending stay for an event type of times is 400 not_a_stay.
Request body (required)
| Field | Type | Description |
|---|---|---|
| eventTypeId * | string | Must be an active, non-deleted event type owned by the authenticated user. |
| startTime | string | Meeting start. Required unless stay is sent. Anything the runtime can parse as a date-time; normalised to UTC before it is stored. |
| endTime | string | Meeting end. Required unless stay is sent. Normalised to UTC before it is stored. |
| attendeeName * | string | |
| attendeeEmail * | string | |
| notes | string | Free text stored as the booking's attendee notes. |
| status | string | Status 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 | boolean | Create 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. |
| attendeeLocale | string | The attendee's language, as a BCP 47 tag in the form es or es-MX (a two-letter language, optionally a two-letter region or 419); case and an underscore separator are tolerated (ES_mx). The language must be one Calemander has a catalog for (en, es). Stored with the booking and used by the attendee's booking pages and by any later message, each in that language once it is available for that page or message and in English until then; English is always stored as en-US. Omit it to fall back to this request's Accept-Language header (its first entry in a supported language); with neither, no language is stored and English is used. A value that is not a supported tag is refused with 400. Not echoed in the response. |
| stay | object | The dates of a stay, for an event type that sells nights or days, instead of startTime/endTime. Planned and claimed through the same inventory ledger as the booking page: the type's capacity, closed dates, its closed-to-arrival and closed-to-departure dates and minimum stays, and the event type's shortest and longest stay all apply; the event type's minimum notice and booking window do not (the credential books as the host), and an arrival in the past is refused. A full or closed period answers 409 stay_unavailable. No money is taken: the quoted price is the stay's balance, collected at the desk or later. A stay booked here carries no late check-out fee and no balance charged before arrival, because the guest was shown neither. |
{
"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
| Status | Body | Description |
|---|---|---|
| 201 | — | Booking created. |
| 400 | Error | A required field is missing, the email is not valid, a time could not be parsed, or |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No active, non-deleted event type with that id belongs to the authenticated user. |
| 409 | Error | The slot could not be claimed — something else holds that time. For a stay: a period is full or closed ( |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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 a booking within this credential's reach — any host's in the workspace for an admin credential, the credential owner's own for a member. An admin cancelling a booking someone else hosts cleans up that host's calendar and meeting and mails the attendee from their identity; the credential is recorded as the actor. 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.
Stays. This endpoint never changes a stay's dates; use POST /api/v1/bookings/{id}/stay-change. A stay's no-show goes through POST /api/v1/bookings/{id}/no-show, which sets its nights aside so the mark can be taken back, and a stay that no longer holds its nights cannot be put back by a status: both are 409 stay_status_not_supported.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| status | "confirmed" | "canceled" | "pending" | "no_show" | The only four values this endpoint accepts. Anything else is a 400. |
| attendeeNotes | string or null | Replaces the notes. An empty string clears them. |
{
"status": "canceled"
} Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Booking updated. |
| 400 | Error | The status is not one of the four allowed values, or neither field was supplied. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id belongs to the authenticated user. |
| 409 | Error | 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. For a stay, a status change it does not support ( |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | NoShowResult | The booking is now marked as a no-show. |
| 400 | Error | The booking is not confirmed, or its start time is still in the future. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | NoShowResult | The no-show mark has been withdrawn. |
| 400 | Error | The booking is not marked as a no-show. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id belongs to the authenticated user. |
| 409 | Error | The time the mark released has since been booked by somebody else, so the booking cannot be put back. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Request body
| Field | Type | Description |
|---|---|---|
| expiresInHours | integer | Defaults to the event type's retimeWindowHours, or 336 when that is 0. |
Responses
| Status | Body | Description |
|---|---|---|
| 201 | — | The link and when it expires. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id belongs to the authenticated user. |
| 409 | Error |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| slug * | path | string | Event type slug. Must be active, not deleted, and owned by the authenticated user. |
| date * | query | string | The date to check, YYYY-MM-DD. Rejected with 400 if absent or not that exact shape. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Bookable slots for that date. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The event type exists but belongs to a different user, or the credential lacks the |
| 404 | Error | No active, non-deleted event type has that slug. |
| 409 | Error | The event type sells nights or days rather than times (a stay), so it has no slots. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| search | query | string | Case-insensitive substring match against name, email or company. % and _ in the value are SQL LIKE wildcards and are not escaped. |
| tag | query | string | Only 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 | query | integer | 1-based page number. Values below 1 and unparseable values fall back to 1. |
| limit | query | integer | Rows per page. Clamped to 1–100; unparseable values fall back to 50. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | A page of contacts. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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.
GET /api/v1/one-off-links
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
| Name | In | Type | Description |
|---|---|---|---|
| event_type_id | query | string | Only links for this event type. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Up to 100 links. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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)
| Field | Type | Description |
|---|---|---|
| event_type_id | string | |
| event_type_slug | string | Used only when event_type_id is absent. |
| label | string | Free text to identify the link in listings. Not shown to the invitee. |
| expires_at | string | When the link stops working. Must parse as a date and be in the future. |
| max_uses | integer | How many bookings the link accepts before it is spent. |
{
"event_type_slug": "onboarding-call",
"label": "Order 10482",
"max_uses": 1
} Responses
| Status | Body | Description |
|---|---|---|
| 201 | — | Link created. |
| 400 | Error | Body was not JSON, neither identifier was supplied, |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No active, non-deleted event type matching that id or slug belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Status | Body | Description |
|---|---|---|
| 200 | — | Visible subscriptions. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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)
| Field | Type | Description |
|---|---|---|
| url * | string | Where 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 WebhookEventName | Events 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
| Status | Body | Description |
|---|---|---|
| 201 | WebhookSubscription & object | Subscription created. This is the only response that carries |
| 400 | Error | Body was not JSON, |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential lacks the |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Subscription id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | WebhookSubscriptionDetail | The subscription. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such subscription, or it is not one this user manages. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Subscription id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Deleted. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such subscription, or it is not one this user manages. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Status | Body | Description |
|---|---|---|
| 200 | — | The current theme. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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)
| Field | Type | Description |
|---|---|---|
| theme_preset | string | Panel preset: glass, glass-dark, minimal, colorful or dark. |
| accent_color | string | Button and link color, #rrggbb. |
| font_family | string | A font from the catalog. Any other name is refused. |
| border_radius | string | Corner radius, 0px to 32px. |
| background_type | string | gradient, solid, preset, image or video. |
| background_value | string | The color, preset name or URL the type calls for. |
| panel_tint | string | Card color behind the booking form, #rrggbb. An empty string clears it back to the preset’s own card. |
| heading_font_family | string | A font from the catalog for headings. An empty string clears it, so headings use font_family. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The theme as it now is. |
| 400 | — | Invalid field, or |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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)
| Field | Type | Description |
|---|---|---|
| url * | string | The public https address of the website to read. At most 2048 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | A proposal, or a note saying why the site could not be read. |
| 400 | Error | No url, or one too long to be a website address. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| page | query | integer | 1-based page number. Values below 1 and unparseable values fall back to 1. |
| limit | query | integer | Rows per page. Clamped to 1–100; unparseable values fall back to 50. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | One page of history. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | The proposal id, from a match or from the history list. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The proposal as stored. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such proposal for this user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | The proposal to apply. |
Request body
| Field | Type | Description |
|---|---|---|
| fields | array of string | Proposal field names to accept, e.g. accent_color. Omit to accept all of them. |
| overrides | object | Your own values, applied over the proposal. Theme fields only. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The theme as it now is. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such proposal for this user. |
| 409 | Error | The proposal was already applied. |
| 422 | — | The proposal cannot be applied, or |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | The applied proposal to undo. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The restored theme. |
| 400 | Error | The theme saved before the apply is no longer valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such proposal for this user. |
| 409 | Error | The proposal was never applied, or has no snapshot. |
| 422 | Error | The snapshot saved before the apply cannot be read. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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)
| Field | Type | Description |
|---|---|---|
| url * | string | The public https address of the website to read. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | A preview link and when it stops working. |
| 400 | Error | Not a JSON body, no |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 422 | — | The site could not be read. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | The workspace's database or cache binding was not available to serve the request. |
| 503 | Error | 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)
| Field | Type | Description |
|---|---|---|
| eventTypeId * | string | The event type the customer is buying. Must carry a checkout requirement. At most 128 characters. |
| requirementKey | string | The checkout requirement's key. Optional; refused if it is not the one this event type carries. At most 64 characters. |
| attendeeEmail | string | Bind the token to one address. The booking must then be made with it. At most 320 characters. |
| claimTtlSeconds | integer | How long the reservation lasts once made, 300–7200. Default 1800. |
| tokenTtlSeconds | integer | How long this token may be used to start a reservation, 300–3600. Default 1800. |
Responses
| Status | Body | Description |
|---|---|---|
| 201 | — | The token, its expiry, and the embed URL to mount. |
| 400 | Error | The event type has no checkout requirement, carries a Calemander price, or a TTL is out of range. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such event type in this workspace. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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.
A credential may report the payment when its owner is a workspace admin, owns the reservation's event type, or is the host the booking is assigned to, so a round-robin booking Calemander assigned to a host other than the one whose key reserved the time still settles with that key. Any other credential gets 404 not_a_claim.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id — the one the booker gave you when the time was reserved. |
| Idempotency-Key | header | string | Any string up to you. A retry carrying the same key replays the first answer instead of writing twice. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| provider * | string | Your label for who took the money. 1–40 characters of lowercase letters, digits, hyphens or underscores. Letters are lowercased before the check. |
| reference * | string | Your own reference for the payment, 1–200 characters. |
| amountCents | integer | What was paid, in the event type's currency. A non-negative whole number. |
| currency | string | Must match the event type's currency, or be omitted. At most 10 characters. |
| receiptUrl | string | A link a person can follow to the receipt. At most 2000 characters. |
| paidAt | string | When the payment cleared, ISO-8601. At most 40 characters. |
| note | string | A line for the host. Never shown to the customer. At most 2000 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Recorded, or already recorded. |
| 400 | Error | The body cannot be acted on: no provider or reference, a bad amount, or the wrong currency. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | ClaimError |
|
| 409 | ClaimError |
|
| 410 | ClaimError |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | The workspace's database or cache binding was not available to serve the request. |
POST /api/v1/bookings/{id}/lock-claim
Lock a picked time while the customer pays
Requires the bookings:write scope.
Takes the time for the length of a payment session, immediately BEFORE you create that session.
Only meaningful on an event type with claimExclusive: false, where a customer's pick reserves nothing and the time stays on sale. This is what turns that pick into a reservation, and it is the reason a customer never pays for a time somebody else already took: a caller that loses here has charged nobody.
409 slot_taken means the time went while the customer was typing. The pick is cancelled, nothing has been charged, and you send them back to the picker.
The deadline is written to the claim's own expiry, so the time comes back on sale by itself if nobody pays, and release-claim gives it back sooner. Locking again extends the deadline and never shortens it.
On an event type with claimExclusive: true this is a no-op that reports the deadline the reservation already has, so you may wire the call once and leave it wired.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id of the pick to lock. |
Request body
| Field | Type | Description |
|---|---|---|
| lockTtlSeconds | integer | How long the lock lasts, in seconds. Set it to the life of your payment session: a lock that outlives the session holds a time nobody can buy, and one that dies before it puts the customer back in the race. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The time is locked, or was already. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | ClaimError | No such booking in this workspace, or |
| 409 | ClaimError |
|
| 410 | ClaimError |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id of the reservation to give back. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Whether this call was the one that released it. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such booking in this workspace. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | BookingRequirementList | The booking’s requirements. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
| key * | path | string | The requirement’s key, as defined on the event type. |
| Idempotency-Key | header | string | Any string up to you. A retry carrying the same key replays the first answer instead of writing twice. |
Request body
| Field | Type | Description |
|---|---|---|
| externalRef | string | The identifier your system matched on — a county application number, a policy number, a certificate id. At most 200 characters. |
| evidenceUrl | string | A link your system can supply for a person who wants to look at the underlying record. At most 2000 characters. |
| note | string | A line for the host. Never shown to the attendee. At most 2000 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | RequirementWriteResult | The requirement’s state after the call, and the booking’s. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such booking for this credential, or the booking has no requirement with that key. |
| 409 | ClaimError |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
| key * | path | string | The requirement’s key, as defined on the event type. |
| Idempotency-Key | header | string | Any string up to you. A retry carrying the same key replays the first answer instead of writing twice. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| note * | string | Why this booking does not need it. Required. At most 2000 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | RequirementWriteResult | The requirement is waived. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such booking for this credential, or the booking has no requirement with that key. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
| key * | path | string | The requirement’s key, as defined on the event type. |
| Idempotency-Key | header | string | Any string up to you. A retry carrying the same key replays the first answer instead of writing twice. |
Request body
| Field | Type | Description |
|---|---|---|
| externalRef | string | The identifier your system matched on, compared against what the attendee typed. At most 200 characters. |
| note | string | A line for the host. Never shown to the attendee. At most 2000 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | RequirementWriteResult | The submitted reference is verified and the requirement is cleared. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such booking for this credential, or the booking has no requirement with that key. |
| 409 | Error | The requirement is not in the |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
| key * | path | string | The requirement’s key, as defined on the event type. |
| Idempotency-Key | header | string | Any string up to you. A retry carrying the same key replays the first answer instead of writing twice. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| note * | string | What was wrong with it. Required, and host-facing only. At most 2000 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | RequirementWriteResult | The submission has been sent back. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such booking for this credential, or the booking has no requirement with that key. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | Any string up to you. A retry carrying the same key replays the first answer instead of writing twice. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| key * | string | The requirement key this clears. At most 64 characters. |
| eventTypeId | string | Limit it to one event type. Omit to clear the key on any event type in the workspace that defines it. At most 64 characters. |
| subject * | object | Who the clear is about. At least one field is required. |
| externalRef | string | The identifier your system matched on. Defaults to the subject’s reference. At most 200 characters. |
| evidenceUrl | string | A link to the underlying record. At most 2000 characters. |
| note | string | A line for the host. Never shown to the attendee. At most 2000 characters. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | Preclearance | A replay of the same |
| 201 | Preclearance | The pre-clearance is on file and waiting for a booking. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No event type with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Event type id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | EventTypeRequirementList | The event type’s requirement definitions. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No event type with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Event type id. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| requirements * | array of EventTypeRequirement | The complete list, in the order attendees should read it. At most 20. An empty array clears them. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | EventTypeRequirementList | The event type’s requirements as they now stand. |
| 400 | Error | The request body is not valid. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No event type with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Event type id. |
| stage | query | "at_booking" | "after_booking" |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | QuestionList | The event type’s questions. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No event type with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Event type id. |
Request body (required)
| Field | Type | Description |
|---|
Responses
| Status | Body | Description |
|---|---|---|
| 201 | — | The question that was created. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No event type with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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. A requirement bound to this question (evidenceQuestionId) is stranded by a move to after_booking or a change to a type nobody types into; that is refused with 422 requirement_binding_in_use unless the body sets detachRequirements to true.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Event type id. |
| questionId * | path | string | Question id. |
Request body (required)
| Field | Type | Description |
|---|
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The updated question. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such question on an event type belonging to the authenticated user. |
| 422 | Error |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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. A question a requirement takes its reference from is refused the same way; pass ?detachRequirements=true to unhook the binding and delete it anyway, after which the attendee is asked for the reference on the checklist instead.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Event type id. |
| questionId * | path | string | Question id. |
| detachConditions | query | boolean | Set to true to drop the conditions that depend on this question instead of refusing with 422. |
| detachRequirements | query | boolean | Set to true to unhook the requirement bindings that name this question instead of refusing with 422. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The question is no longer asked. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No such question on an event type belonging to the authenticated user. |
| 422 | Error |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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, unless purgedAt is set — then it was answered and the answer has since been deleted under the workspace's intake retention policy. answeredAt is null for an answer given on the booking form.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | BookingResponseList | The booking’s answers. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id belongs to the authenticated user. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| responses * | array of object |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | BookingResponseList | The full answer set after the merge. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error |
|
| 409 | Error |
|
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | The workspace's database or cache binding was not available to serve the request. |
Stays
Event types that sell nights or days instead of times: the inventory they sell (rooms, pitches, bays, places in a pool) and what is left on each date.
GET /api/v1/inventory-types
List inventory types
Requires the event_types:read scope.
The kinds of thing the workspace sells by the night or by the day (rooms, pitches, bays, places in a pool), each with its active units. Types are shared by the whole workspace, like rooms, so every credential that may read event types lists all of them. Archived types are left out.
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | Every active inventory type. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | The workspace's database or cache binding was not available to serve the request. |
GET /api/v1/inventory-types/{id}
Get an inventory type
Requires the event_types:read scope.
One inventory type and its active units.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Inventory type id. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | — | The inventory type. |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No active inventory type with that id ( |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | The workspace's database or cache binding was not available to serve the request. |
GET /api/v1/stays/availability
What is left on each date of a stay
Requires the availability:read scope.
For an event type that sells nights or days, what is left on each date of a window of at most 62 days, per inventory a booker can choose: closed dates, dates a stay may not start or end on, a date's minimum stay, each date's price and the length-of-stay discounts. The same answer the booking page's date picker reads. Book a stay with POST /api/v1/bookings and stay. An event type of times answers 409 not_a_stay; its slots are GET /api/v1/availability/{slug}.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| event_type_id * | query | string | One of the credential's own event types. |
| from * | query | string | The first date, YYYY-MM-DD. |
| to * | query | string | The last date, inclusive, YYYY-MM-DD. At most 61 days after from. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | StayAvailability | Every date in the window. |
| 400 | Error |
|
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No event type with that id belongs to the credential's user. |
| 409 | Error | The event type sells times, not nights or days ( |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | The workspace's database or cache binding was not available to serve the request. |
POST /api/v1/bookings/{id}/stay-change
Change a stay's dates or inventory type
Requires the bookings:write scope.
Moves a stay: extend, shorten, shift, leave early, or change its inventory type. Periods the stay already held keep the price they were booked at and only new ones take today's rates, unless reprice. Send it first with preview (the default) to see the plan and the difference, then again with preview: false and that difference as expectedDifferenceCents to apply it: the inventory moves in one step or not at all (409 stay_unavailable naming the periods that no longer fit), the guest is emailed, reminders follow the new dates and booking.rescheduled fires with the old and new dates. A difference that moved since the preview is refused (409 stay_change_quote_changed).
Once checked in, a stay's arrival and type cannot change; a stay that checked out, one awaiting its payment, and one due to arrive already that never checked in cannot change at all.
Money stays on the stay. moneyAction is balance (the default when it costs more: the difference is owed) or none (the default when it costs less: any overpayment is left for staff to refund). Charging a saved card, sending a pay link and refunding are done from the dashboard.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id * | path | string | Booking id. |
Request body (required)
| Field | Type | Description |
|---|---|---|
| arrivalDate | string | The new first night (or day). Omit to keep it. |
| departureDate | string | The new departure. Omit to keep it. |
| inventoryTypeId | string | Move the stay to another inventory the event type sells. Omit to keep it. |
| reprice | boolean | Price every period at today's rates instead of keeping booked prices. |
| preview | boolean | Default true: answer the plan and write nothing. false applies it. |
| expectedDifferenceCents | integer | Required to apply: the preview's money.differenceCents. |
| moneyAction | "balance" | "none" | What to do about the difference when applying. See the description. |
Responses
| Status | Body | Description |
|---|---|---|
| 200 | object or object | The plan ( |
| 400 | Error | Dates that are malformed or not allowed for this stay, nothing to change ( |
| 401 | Error | No credential was presented, or it did not resolve. |
| 403 | Error | The credential resolved but lacks the scope this operation requires. The message names the missing scope. |
| 404 | Error | No booking with that id within the credential's reach. |
| 409 | Error | The booking is not a stay ( |
| 429 | RateLimitError | More than 60 requests in the current 60-second window. Note the body shape differs from every other error. Three other 429s share this status with different bodies: an OAuth client over its own per-minute or daily write budget answers |
| 500 | Error | 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.
A stay that staff (or POST /api/v1/bookings/{id}/stay-change) moved carries its new dates in stay and the old ones in oldStay; oldStartTime/oldEndTime are the old check-in and check-out instants.
{
"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.
booking.checked_out
A stay checked out
Fires when staff, or a guest at a door screen, check out a stay that was checked in. The booking keeps its status; the payload adds checkedOutAt and checkedOutBy (staff or door), and carries the booking's stay. A second check-out of the same stay sends nothing. Not sent to a calendly-format subscriber: Calendly has no attendance events.
Schemas
Booking
| Field | Type | Description |
|---|---|---|
| id * | string | |
| eventTypeId * | string | |
| eventTypeName * | string or null | Null if the event type row is gone. |
| eventTypeSlug * | string or null | |
| hostId | string or null | The user who hosts this booking. On a round-robin booking that is the assigned member, not the event type's owner — the same person the webhook payload's host names. Pass it back as ?host= to narrow a list. |
| hostEmail | string or null | That host's email address. |
| startTime * | string | UTC, as stored. |
| endTime * | string | UTC, as stored. |
| attendeeName * | string | |
| attendeeEmail * | string | |
| attendeeNotes * | string or null | |
| attendeeTimezone * | string or null | IANA timezone the attendee booked in, e.g. Europe/London. Null on rows recorded before the column existed. |
| status * | string | Values in use: confirmed, pending, canceled, rescheduled, payment_pending, no_show. |
| meetingUrl * | string or null | |
| paymentStatus * | string or null | Null for free event types. |
| guestEmails * | array of string | Additional invitees. Empty when there are none. |
| createdAt * | string | |
| detailsUrl | string or null | The 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 null | When an after-booking answer on this booking last changed. Null when every answer came in with the booking. |
| tip | object or null | A gratuity the attendee added at checkout. Part of what the provider charged and NOT part of the booking's own total, so reconcile a payout by subtracting it before comparing anything to the service price. Null when the booking was not tipped. Read-only: a host cannot tip on their client's behalf, so there is no write edge. |
| stay | null or Stay | The dates, inventory, units and charges of a booking that sells nights or days; null for a booking of a time. |
| paymentProvider | string or null | Which rail this booking’s money is on, if any. external means your own checkout took the payment and Calemander only recorded it. |
| externalPayment | object or null | What your checkout reported through POST /bookings/{id}/external-payment, or null. Never a Calemander charge. |
| claimExpiresAt | string or null | When an unpaid reservation gives its time back. Null on every other booking. |
BookingRequirement
One prerequisite on one booking, with the full record of how it reached its state.
| Field | Type | Description |
|---|---|---|
| key | string | The 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 | string | The attendee-visible name, snapshotted when the booking was made. |
| instructions | string or null | Attendee-visible body text, snapshotted. |
| instructionsUrl | string or null | Where 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 null | What the attendee typed, when they were allowed to supply a reference. |
| evidenceSource | "form_answer" | "attendee_page" | "api" | null | Where evidenceRef came from. form_answer means the attendee typed it into the bound booking question before the booking existed; attendee_page means they typed it on the self-serve checklist; api means a credential submitted it for them. Null when nothing has been submitted. Recorded, never inferred. |
| externalRef | string or null | The clearing system’s own identifier for what it matched. |
| clearedAt | string or null | |
| clearedBy | "host" | "attendee" | "api" | "system" | null | Which kind of credential cleared it. Set by the server, never by the request. |
| clearMethod | "system_reference" | "host_manual" | "attendee_evidence" | null | How it was cleared. Derived from the row’s state and the credential; there is no request field for it. |
| verifiedBy | "api" | "host" | null | On an attendee_evidence clear, which kind of verifier said yes. |
| referenceMatched | boolean | True when the clearing system’s reference is the one the attendee had already typed — two independent sources naming the same file. |
| note | string or null | Why it was waived or sent back. Written for the host and never shown to the attendee. |
| sortOrder | integer |
BookingRequirementList
| Field | Type | Description |
|---|---|---|
| bookingId | string | |
| outstanding | integer | How many requirements are still neither cleared nor waived. |
| requirements | array of BookingRequirement |
BookingResponse
| Field | Type | Description |
|---|---|---|
| questionId | string | |
| question | string | The question's label. |
| answer | string or boolean or array of string or null | The stored answer, or null when the question has not been answered. |
| stage | "at_booking" | "after_booking" | |
| answeredAt | string or null | Null for an answer given on the booking form. |
| purgedAt | string or null | When this answer was deleted under the workspace's intake retention policy, or null while it is still held. Tells the two kinds of answer: null apart: never given, and given and since deleted. |
BookingResponseList
| Field | Type | Description |
|---|---|---|
| bookingId | string | |
| responses | array of BookingResponse | |
| detailsUrl | string or null | The attendee's booking page, when the event type asks anything after booking. |
| changedQuestionIds | array of string | Returned only by PUT: the questions whose answers this call changed. |
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>.
| Field | Type | Description |
|---|---|---|
| event * | WebhookEventName | |
| payload * | BookingWebhookPayload | |
| timestamp * | string | When 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.
| Field | Type | Description |
|---|---|---|
| bookingId * | string | |
| eventSlug * | string | |
| eventName * | string | |
| startTime * | string | UTC. |
| endTime * | string | UTC. |
| attendeeName * | string | |
| attendeeEmail * | string | |
| status * | string | |
| oldStartTime | string | The time the booking moved from. Present on booking.rescheduled only. |
| oldEndTime | string | Present on booking.rescheduled only. |
| eventTypeId | string | |
| eventTypeSlug | string or null | |
| eventTypeName | string or null | |
| durationMinutes | integer | The event type's duration for a booking of a time. For a stay, the booking's own span from check-in to check-out in minutes. |
| locationType | string or null | |
| meetingUrl | string or null | |
| attendeeNotes | string or null | |
| attendeeTimezone | string or null | IANA timezone the attendee booked in, e.g. Europe/London. |
| host | null or object | The 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 object | The invitee's answers to the event type's booking questions, in question order. |
| releaseAt | string or null | When a held booking is released if nothing clears it, ISO-8601 UTC. Present on booking.held, and on booking.created for a booking that parked on pending. Null when the event type runs no release clock, and null on a confirmed booking — never a guess. |
| requirementsUrl | string or null | The attendee's own checklist page. Present on booking.held; null when the only outstanding item is the host's own approval, in which case there is nothing there for them and it must not be relayed. |
| requirements | null or array of object or object | What the booking is waiting on. An array of items on booking.held; a summary object on booking.created for a booking that parked; null on a confirmed booking. submitted is not done — it is a claim awaiting a verifier. |
| bookingCreatedAt | string | |
| canceledBy | string or null | |
| cancellationReason | string or null | |
| paymentStatus | string or null | |
| guestEmails | array of string | |
| joinUrl | string or null | The 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 null | The 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 null | The 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. |
| tip | object or null | A gratuity on this booking's charge, or null. Carried on booking.confirmed as a property of the booking rather than as an event of its own: a subscriber that writes a row on booking.confirmed must not get a second row saying the same booking was tipped. |
| stay | null or Stay | The dates, inventory, units and charges of a booking that sells nights or days; null for a booking of a time. |
| oldStay | object | The dates a stay moved from. Present on booking.rescheduled for a stay only; the new dates are stay. |
| checkedOutAt | string | When the stay checked out. Present on booking.checked_out only. |
| checkedOutBy | "staff" | "door" | Who checked it out: staff on the stays board, door for a guest at a paired door screen. Present on booking.checked_out only. |
ClaimError
The Error shape with the facts a checkout needs to act on: a refusal about a reservation made through your own checkout, or a requirement write refused for the same reason. Every field but message is present only when the refusal has it.
| Field | Type | Description |
|---|---|---|
| message * | string | Human-readable explanation. |
| code | string | not_a_claim, in_flight, slot_taken, claim_expired or record_payment_instead. |
| bookingId | string | |
| bookingStatus | string | The booking’s status now, e.g. canceled. |
| startTime | string | |
| endTime | string | |
| rebookUrl | string | Where the customer can choose a new time. |
| recorded | boolean | Whether the payment was recorded even though the time is gone. Always false from a lock, which happens before any payment. |
ClientBudgetError
A 429 from an OAuth client’s own budget, which every workspace the client is installed in shares. Names the budget that refused.
| Field | Type | Description |
|---|---|---|
| error * | string | client_write_budget_exceeded (the daily write budget) or client_rate_limit_exceeded (the per-minute rate). |
| message * | string | |
| budget | string or null | daily_write_budget or rate_per_min. |
| limit | integer or null | The configured number that was hit. |
| category | string | The client’s category, which sets its budgets. |
| retryAfter * | integer | Unix timestamp (seconds) at which the refusing window resets. |
Contact
| Field | Type | Description |
|---|---|---|
| id * | string | |
| name * | string | |
| email * | string | Null for a contact created without an address. |
| phone * | string or null | |
| company * | string or null | |
| tags * | array of string | |
| source * | string | How the contact was first recorded. |
| bookingCount * | integer | Lifetime counter on the contact row, incremented when a booking is linked and never recomputed. It is NOT a live count of this contact's booking rows, and since 2026-09-21 the dashboard no longer uses it: Contacts and the contact page both count bookings keyed on the contact, so this field can be higher than what a host sees. Count bookings with GET /api/v1/bookings filtered by the contact if you need the live number. Tracked as issue #168. |
| lastBookingAt * | string or null | |
| createdAt * | string | |
| phoneNormalized | string or null | The phone number in E.164 form, when it could be read as one. |
Error
The error shape used by every endpoint except the per-credential 429.
| Field | Type | Description |
|---|---|---|
| message * | string | Human-readable explanation. |
| code | string | A machine-readable reason, present when the refusal has one. Branch on this rather than on message, whose wording may change. |
EventType
| Field | Type | Description |
|---|---|---|
| id * | string | |
| name * | string | |
| slug * | string | Used in booking URLs and in the availability path. |
| duration * | integer | Meeting length in minutes. |
| maxDaysAhead | integer or null | How 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 null | The raw stored legacy setting. Prefer locationType and calendarProvider, which are the resolved values. |
| locationType * | string or null | Where the meeting happens, resolved against the owner's defaults. |
| calendarProvider * | string or null | Which 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 EventTypeHost | Active member hosts. Empty for one_on_one event types. |
| maxSeats * | integer or null | Seats per slot for group events; null for one-attendee events. |
| capacity * | integer or null | Seats 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 null | The event type's #rrggbb accent colour. |
| autoCloseMinutes * | integer or null | For 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 | boolean | Whether a full class offers a place in line instead of a dead end. |
| standbyOfferMinutes | integer | How long one person holds a freed seat before it passes to the next. |
| redirectUrl * | string or null | Where an invitee is sent after booking, if configured. |
| redirectPassDetails * | boolean | Whether booking details are appended to redirectUrl as query parameters. |
| sendHeldNotifications * | boolean | Whether 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. |
| claimExclusive * | boolean | Whether a claim made through your own checkout takes the time off sale while the customer pays. Defaults to true. Set it to false to leave the time bookable by anybody until you report a payment against the claim: the pick is still recorded, still expires, and still seeds its requirements, but it does not reserve anything. The first payment reported for a time wins; a later one is answered 409 slot_taken, with the payment recorded and the booking cancelled. Only meaningful on an event type that carries a claimable requirement. |
| priceCents * | integer or null | Price in minor units; null for free event types. |
| currency * | string | Defaults 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 null | The 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 null | The 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. |
| requirementsHoldHours | integer or null | Release a held booking this many hours after it was made. Null (the default) means this clock is off. With both clocks off, a hold on a requirement you defined runs for 72 hours unless the attendee has paid, in which case it is kept until someone acts. |
| requirementsCutoffHours | integer or null | Release a held booking this many hours before it starts. Null (the default) means this clock is off. Whichever of the two clocks comes first wins. |
| requirementsNudgeHours | integer or null | Remind the attendee this many hours before the hold is released. Null or 0 means no reminder. A lead that cannot fit inside the hold window is refused with 422: the engine will not queue a reminder unless the whole window is at least the lead plus two hours, so such a nudge would never be sent. |
| releaseMessageOverride | string or null | Your own sentence in the email an attendee gets when their held time is released, and in the one that follows a requirement clearing too late. It is added AFTER the two facts Calemander states — the time is gone, and no refund was issued — and replaces neither. Null (the default) adds nothing. The host's own copy of the release email never carries it. |
| sendReleasedNotifications | boolean | Whether Calemander tells the attendee their held time was released. True by default. False silences the attendee's released email, its text message and the late-clear email, and is refused with 422 unless this workspace has an active webhook subscription for booking.released — a tenant announcing the release itself has to be able to hear it. If that subscription is later removed, this switches back on. Your own copy of the release email sends either way. |
| detailsNudgeHours | integer | Hours before the start to remind the attendee about a required after-booking question still left blank. 0 never reminds. Only fires on an event type that asks one. |
| retimeWindowHours | integer | How long after a held booking is released the attendee may pick a new time with the link they are sent. 0 turns the link off, and the released message points at the public booking page instead. |
EventTypeHost
| Field | Type | Description |
|---|---|---|
| userId * | string | |
| name * | string | |
| email * | string | |
| weight * | number | This host's target share of round-robin assignments. |
EventTypeRequirement
A requirement as defined on an event type — the template copied onto every booking.
| Field | Type | Description |
|---|---|---|
| key | string | Lowercase letters, digits, hyphens or underscores, starting with a letter or digit, at most 64 characters. |
| 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 | Days a pre-clearance lasts, 1–3650. |
| 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. |
| evidenceQuestionId | string or null | The booking-form question whose answer IS this requirement's reference. When it is set and the attendee answers that question, the requirement is seeded submitted with their answer as evidenceRef and the checklist does not ask for it again; a later clear carrying the same reference records referenceMatched. Only valid on an external requirement with allowAttendeeEvidence true, pointing at an active at_booking question of type text, textarea, email or phone on the same event type. Null means nothing is bound, which is the default. |
| sortOrder | integer | |
| claimable | boolean | Payment is collected on your own site: the one requirement your checkout reserves a time against. At most one per event type, and only on an external requirement with gate: hold. |
EventTypeRequirementList
| Field | Type | Description |
|---|---|---|
| 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.
| Field | Type | Description |
|---|---|---|
| name | string | |
| slug | string | Lowercase 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 | Between 5 and 480, in whole 5-minute steps; anything else is refused with 400. |
| maxDaysAhead | integer or null | How 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 null | Above 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. Must be 1 or more when sent as a number. |
| 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 | integer | Seats 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" | null | Set 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 string | Host 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 | boolean | Defaults 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 null | Group 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 | boolean | Group 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 | integer | How 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 | boolean | Defaults 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. |
| claimExclusive | boolean | Whether a claim made through your own checkout takes the time off sale while the customer pays. Defaults to true. Set it to false to leave the time bookable by anybody until you report a payment against the claim: the pick is still recorded, still expires, and still seeds its requirements, but it does not reserve anything. The first payment reported for a time wins; a later one is answered 409 slot_taken, with the payment recorded and the booking cancelled. Only meaningful on an event type that carries a claimable requirement. |
| 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 null | The 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 null | The 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. |
| requirementsHoldHours | integer or null | Release a held booking this many hours after it was made. Null (the default) means this clock is off. With both clocks off, a hold on a requirement you defined runs for 72 hours unless the attendee has paid, in which case it is kept until someone acts. |
| requirementsCutoffHours | integer or null | Release a held booking this many hours before it starts. Null (the default) means this clock is off. Whichever of the two clocks comes first wins. |
| requirementsNudgeHours | integer or null | Remind the attendee this many hours before the hold is released. Null or 0 means no reminder. A lead that cannot fit inside the hold window is refused with 422: the engine will not queue a reminder unless the whole window is at least the lead plus two hours, so such a nudge would never be sent. |
| releaseMessageOverride | string or null | Your own sentence in the email an attendee gets when their held time is released, and in the one that follows a requirement clearing too late. It is added AFTER the two facts Calemander states — the time is gone, and no refund was issued — and replaces neither. Null (the default) adds nothing. The host's own copy of the release email never carries it. |
| sendReleasedNotifications | boolean | Whether Calemander tells the attendee their held time was released. True by default. False silences the attendee's released email, its text message and the late-clear email, and is refused with 422 unless this workspace has an active webhook subscription for booking.released — a tenant announcing the release itself has to be able to hear it. If that subscription is later removed, this switches back on. Your own copy of the release email sends either way. |
| detailsNudgeHours | integer | Hours before the start to remind the attendee about a required after-booking question still left blank. 0 never reminds. Only fires on an event type that asks one. |
| retimeWindowHours | integer | How long after a held booking is released the attendee may pick a new time with the link they are sent. 0 turns the link off, and the released message points at the public booking page instead. |
Identity
| Field | Type | Description |
|---|---|---|
| user * | object | |
| reach * | "workspace" | "own" | How far this credential's booking reads and writes go: workspace for a credential whose owner is a workspace admin (every host's bookings), own for a member (only bookings that member hosts). Derived from the live role in user.role, so it can change without the credential changing. |
| workspace * | object | |
| auth * | object | Which credential type authenticated this request. |
| api_key * | null or object | Describes the API key that authenticated. Null for an OAuth caller, which has no key. |
InventoryType
A kind of thing an event type sells by the night or by the day. Workspace-shared, like rooms: every credential that may read event types lists all of them.
| Field | Type | Description |
|---|---|---|
| id * | string | |
| name * | string | |
| description * | string or null | |
| grain * | "night" | "day" | night sells [arrival, departure); day sells every day from the first to the last. |
| measure * | "units" | "occupants" | units counts assignable units (listed in units); occupants is a headcount pool with no units. |
| maxOccupancy * | integer | Occupants one unit holds. |
| capacity * | integer | How many are sold per period before any date's own capacity or closure: the type's count, else its active units. |
| parentId * | string or null | A type whose cap also applies (a facility above its areas), or null. |
| locationId * | string or null | The location it belongs to, or null for the whole workspace. |
| sortOrder * | integer | |
| units * | array of InventoryUnit | Its active units, in order. Empty for a pool. |
InventoryUnit
One unit of an inventory type: a room, a pitch, a bay, a run.
| Field | Type | Description |
|---|---|---|
| id * | string | |
| label * | string |
NoShowResult
The outcome of marking or clearing a no-show, including the status the booking now carries.
| Field | Type | Description |
|---|---|---|
| success * | true | |
| id * | string | The booking id. |
| status * | "no_show" | "confirmed" | The booking’s status after the call. |
OneOffLink
| Field | Type | Description |
|---|---|---|
| id * | string | |
| label * | string or null | |
| event_type_id * | string | |
| event_type_name * | string | |
| event_type_slug * | string | |
| url * | string | The 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 * | integer | Bookings made through this link so far. |
| booking_id * | string or null | The booking this link produced, where one is recorded. |
| is_expired * | boolean | Computed at read time: expires_at is set and in the past. |
| is_used * | boolean | Computed at read time: use_count has reached max_uses. |
| created_at * | string |
PageNumber
The page that was served.
Type: integer
Preclearance
| Field | Type | Description |
|---|---|---|
| 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.
| Field | Type | Description |
|---|---|---|
| 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 | |
| acknowledgement | boolean | A consent line: one tick carrying the label as its sentence, which the booking is refused without. Only on a yes_no question, only at the at_booking stage, and always required. |
| inviteAsGuest | boolean | This email question's answer is appended to the booking's guest list, so the person named rides onto the calendar invite. Only on an email question, only at the at_booking stage. Independent of the event type's allowGuests: that toggle governs the booker's own "Add guests" rows. The booker can take the person off before booking. Default false. |
| isRequired | boolean | On 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
| Field | Type | Description |
|---|---|---|
| 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 | |
| acknowledgement | boolean | A consent line: one tick carrying the label as its sentence, which the booking is refused without. Only on a yes_no question, only at the at_booking stage, and always required. |
| inviteAsGuest | boolean | This email question's answer is appended to the booking's guest list, so the person named rides onto the calendar invite. Only on an email question, only at the at_booking stage. Independent of the event type's allowGuests: that toggle governs the booker's own "Add guests" rows. The booker can take the person off before booking. Default false. |
| isRequired | boolean | On 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 | boolean | Apply the write even when another question's condition depends on what it changes, dropping those rules instead of refusing with 422. Defaults to false. |
| detachRequirements | boolean | Apply the write even when a requirement takes its reference from this question, unhooking that binding instead of refusing with 422. Its own flag, separate from detachConditions: they confirm different losses. Defaults to false. |
QuestionList
| Field | Type | Description |
|---|---|---|
| questions | array of Question |
QuestionPatch
Any subset of the question’s fields. Only what you send changes.
| Field | Type | Description |
|---|---|---|
| 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 | |
| acknowledgement | boolean | A consent line: one tick carrying the label as its sentence, which the booking is refused without. Only on a yes_no question, only at the at_booking stage, and always required. |
| inviteAsGuest | boolean | This email question's answer is appended to the booking's guest list, so the person named rides onto the calendar invite. Only on an email question, only at the at_booking stage. Independent of the event type's allowGuests: that toggle governs the booker's own "Add guests" rows. The booker can take the person off before booking. Default false. |
| isRequired | boolean | On 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 | boolean | Apply the write even when another question's condition depends on what it changes, dropping those rules instead of refusing with 422. Defaults to false. |
| detachRequirements | boolean | Apply the write even when a requirement takes its reference from this question, unhooking that binding instead of refusing with 422. Its own flag, separate from detachConditions: they confirm different losses. Defaults to false. |
RateLimitError
Only returned by the 429. Produced by the rate limiter before routing, which is why it does not use message.
| Field | Type | Description |
|---|---|---|
| error * | "Rate limit exceeded" | |
| retryAfter * | integer | Unix 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.
| Field | Type | Description |
|---|---|---|
| bookingId | string | |
| bookingStatus | string or null | The booking’s status after the write. confirmed means this call was the last one. |
| outstanding | integer | |
| requirement | BookingRequirement | |
| confirmed | boolean | True only for the call that won the booking’s confirmation and ran its side effects. |
| meetingUrl | string or null | |
| retimeUrl | string or null | Where the attendee can pick a new time, when this write landed on a booking whose hold was already released and the retime window is still open. Null otherwise. |
| rebookUrl | string or null | The same value as retimeUrl, under its older name. |
| cleared | boolean | Returned only by clear: true when this call moved the requirement, false for a no-op. |
| waived | boolean | Returned only by waive: true when this call moved the requirement, false for a no-op. |
| verified | boolean | Returned only by verify: true when this call moved the requirement, false for a no-op. |
| sentBack | boolean | Returned only by send-back: true when this call moved the requirement, false for a no-op. |
| referenceMatched | boolean | Returned only by verify: whether the reference you sent matched what the attendee supplied. |
RoutingFormSubmissionPayload
| Field | Type | Description |
|---|---|---|
| submissionId * | string | Identifies this delivery. Submissions are not stored, so it is not a fetchable id. |
| routingFormId * | string | |
| routingFormName * | string | |
| routingFormSlug * | string | |
| matched * | boolean | Whether a routing rule matched. |
| isFallback * | boolean | Whether 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:readbookings:writeevent_types:readevent_types:writeavailability:readcontacts:readscheduling_links:readscheduling_links:writewebhooks:readwebhooks:managecheck_in:readcheck_in:writecheck_in:managetheme:readtheme:writerequirements:readrequirements: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.
| Field | Type | Description |
|---|---|---|
| standbyId * | string | The standby entry. |
| eventTypeId * | string | |
| eventName * | string | |
| startTime * | string | Start of the class occurrence the entry is for. |
| endTime * | string | |
| attendeeName * | string | |
| attendeeEmail | string or null | |
| position * | integer | The number the person was given when they joined. Never reused, so it can exceed the length of the line. |
| place | integer or null | Rank among the entries still in line, counting from 1. |
| offerExpiresAt | string | standby.offered only: when the seat stops being held. |
| bookingId | string | standby.promoted only: the booking the promotion produced. |
Stay
A booking of an event type that sells nights or days. The dates are authoritative for inventory and price; startTime/endTime on the booking are the check-in and check-out instants derived from them in the property zone.
| Field | Type | Description |
|---|---|---|
| arrivalDate * | string | The first night (or first day), in the property zone. |
| departureDate * | string | For night: the morning the stay leaves, so 2026-11-02 to 2026-11-05 is three nights. For day: the last day, inclusive. |
| grain * | "night" | "day" | Whether the stay is counted in nights or in days. |
| periods * | integer | Nights (or days) the dates hold. |
| inventoryTypeId * | string or null | The inventory type the stay is in. See GET /api/v1/inventory-types. |
| inventoryTypeName * | string or null | The type's name when the stay was booked or last changed. |
| units * | integer | How many units of the type the stay takes (1 for a place in a pool). |
| occupants * | integer | How many occupants (guests, vehicles, animals, whatever the workspace books) the stay is for. |
| assignedUnits * | array of InventoryUnit | The units the stay is in, in the order it reaches them (a move mid-stay lists both). Empty before a unit is assigned and for a pool, which has no units. |
| needsUnit * | boolean | Some period the stay holds has no unit yet. Staff assign one from the occupancy grid. |
| timezone * | string | The property's IANA zone the dates are in, snapshotted at booking. |
| checkedOutAt * | string or null | When the stay checked out, or null. Check-in is the booking-level booking.checked_in event. |
| charges * | object | What the stay costs and what has been paid, in cents of currency. The total is the price lines (each night at its rate, extra occupants, a length-of-stay discount, a late check-out fee once added) less a discount code, plus tax. Paid counts the booking payment net of refunds and of any tip, plus the stay's own charges (a balance or change charged later) net of refunds. |
StayAvailability
What is left on each date of a window, per inventory a booker can choose. The create step re-checks everything against the ledger, so a stale answer can only be refused, never oversold.
| Field | Type | Description |
|---|---|---|
| computedAt * | integer | Epoch milliseconds. |
| eventTypeId * | string | |
| grain * | "night" | "day" | |
| timezone * | string | The property zone the dates are in. |
| checkInTime * | string | HH:MM, property zone. |
| checkOutTime * | string | HH:MM, property zone. |
| minNights * | integer | The shortest stay, in periods. |
| maxNights * | integer | The longest stay, in periods. |
| currency * | string | |
| earliestArrival * | string | The first arrival the booking page accepts after the event type's notice. The API, which books as the host, may book earlier, never in the past. |
| latestPeriod * | string | The last period the booking page's window reaches. |
| choices * | array of object | The inventories a stay can be booked in. Send one as stay.inventoryTypeId. |
| extrasPerPeriodCents * | integer | |
| deposit * | object | |
| losDiscounts * | array of object | Length-of-stay discounts, shortest first; the longest one a stay reaches applies. |
| lateCheckout * | object or null | The late check-out fee the booking page discloses, and the wall time it starts. A stay booked through the API carries none. |
| periods * | object | Every date in the window, then every choice id, to what is left. |
| full * | array of string | Dates no choice can take one more booking on. |
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.
| Field | Type | Description |
|---|---|---|
| theme_preset * | string | Panel preset: glass, glass-dark, minimal, colorful or dark. |
| accent_color * | string | Button and link color, #rrggbb. |
| font_family * | string | A font from the catalog. Any other name is refused. |
| border_radius * | string | Corner radius, 0px to 32px. |
| background_type * | string | gradient, solid, preset, image or video. |
| background_value * | string | The color, preset name or URL the type calls for. |
| panel_tint | string or null | Card color behind the booking form, #rrggbb. Null uses the preset’s own card. |
| heading_font_family | string or null | A font from the catalog for headings. Null means headings use font_family. |
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.
| Field | Type | Description |
|---|---|---|
| paletteId | string | On a match response only: the stored proposal’s id, the same as proposal_id. |
ThemeProposalSummary
One row of match history.
| Field | Type | Description |
|---|---|---|
| id * | string | |
| source_url * | string | The address that was read, as it was given. |
| source_domain * | string | Host of source_url, for a one-line label. |
| final_url | string | Where the read ended after redirects. |
| platform | string or null | What the site appears to be built with. |
| fetch_note | "ok" | "partial" | "blocked" | "unreachable" | "refused" | |
| status * | "proposed" | "applied" | "reverted" | "dismissed" | |
| applied_fields | array or null | Which fields were accepted, once the proposal was applied. |
| created_at * | string | |
| applied_at | string or null |
TimeSlot
| Field | Type | Description |
|---|---|---|
| start * | string | UTC. |
| end * | string | UTC. |
| best | true | Present 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.createdbooking.canceledbooking.rescheduledbooking.confirmedbooking.no_showbooking.no_show_clearedbooking.checked_inbooking.checked_outbooking.heldrequirement.submittedrequirement.clearedbooking.releasedbooking.responses_updatedrouting_form_submission.createdstandby.offeredstandby.promoted
WebhookSubscription
A subscription as returned by the list and create endpoints.
| Field | Type | Description |
|---|---|---|
| id * | string | |
| url * | string | |
| events * | array of WebhookEventName | |
| scope * | "user" | "organization" | |
| payload_format * | "calemander" | "calendly" | |
| is_active * | boolean | False once the endpoint has answered 410 Gone, or after it was disabled. |
| created_at * | string | |
| user_id | string | The 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.
| Field | Type | Description |
|---|---|---|
| id * | string | |
| url * | string | |
| events * | array of WebhookEventName | |
| scope * | "user" | "organization" | |
| is_active * | boolean | |
| created_at * | string | |
| user_id * | string |