Base URL
All API requests go to a single host.Server specifications
- Encode requests as JSON. Responses, including errors, are JSON.
- HTTP/1, 1.1, and 2 are supported. HTTP/2 is preferred.
- TLS 1.2 and 1.3 are supported, with session resumption.
Authentication
Every request is authenticated with an API key sent as a bearer token. Keys are prefixed withcalid_.
Response format
Successful responses wrap the resource in a JSON envelope, and the payload you want lives underdata. The envelope is built from success, data, message, and meta — but not every endpoint returns all four, and one class of error does not use the envelope at all.
Unknown route or wrong method
Always read the resource out of
data rather than the top level — and do not assume data is an object. On list endpoints, meta carries pagination and contextual information where the endpoint provides it at all.Things that surprise people
These behaviours account for most first-integration bugs. Read them before you debug.List endpoints do not share a default page size — and some ignore paging
There is no single pagination rule. The defaultlimit, the parameter names, and even whether paging works at all vary by endpoint.
GET /contacts/ puts the records in data.rows and the counts in data.meta, using totalRowCount, hasMore, limit, and offset — there is no total, page, or totalPages. Advance offset while data.meta.hasMore is true.
GET /schedule/ and GET /teams/{teamId}/schedules accept page and limit without erroring but ignore them — ?limit=1 still returns the full list — and they return no meta object. A paging loop against these endpoints will never terminate.
Asking for more than the ceiling is rejected rather than clamped:
The
100 ceiling is confirmed on /event-types/, /contacts/, and /teams/*. It was not tested on /booking/ or /webhook/, which already default to 100.GET /users/{userslug} returns an array
Even though it looks up a single user by slug, this endpoint returns data as an array, not an object.
data[0]. Code written as response.data.name will return undefined rather than erroring, which makes this a quiet bug — you get a null-ish value in your UI instead of a stack trace. GET /users/me, by contrast, returns data as an object.
GET /availability/ needs a date and speaks a different date format
Two things set this endpoint apart from the rest of the API.
dateFrom is required. Calling GET /availability/ with no query string returns 400, not an unbounded list.
2026-09-08T03:30:00.000Z. This one returns:
Slots and bookings use different time parameters
The parameter names are not interchangeable between the two endpoints.
Both endpoints take
start and end. Sending startTime/endTime to either is rejected with 400 — body must have required property 'start'. The trap is that booking responses come back with startTime and endTime, so you cannot echo a booking object straight back as a request body.
You cannot compute a bookable time yourself
Availability is the product of schedules, buffers, minimum notice, booking limits, connected-calendar busy times, and timezone rules. Reimplementing that logic will not match Cal ID’s answer.1
Fetch real availability
Call
GET /slots/ with start and end for the window you care about.2
Pick a slot from the response
Choose one of the times the endpoint actually returned.
3
Book that exact time
Send it to
POST /booking/ as start, with a matching end.400 means your request is malformed; 500 can mean it is unbookable
Schema problems return400. A missing field, wrong type, out-of-range value or invalid enum comes back as 400 with an error.code of FST_ERR_VALIDATION, BAD_REQUEST or VALIDATION_ERROR, and a message naming the offending field.
Scheduling-rule failures return 500. A request that is structurally valid but asks for something impossible — most often booking a time that GET /slots/ did not offer — returns 500 with error.code: "INTERNAL_ERROR" and a domain message such as no_available_users_found_error.
This matters for retries: a 500 here is not a transient fault and retrying with backoff will fail identically. Read message before you decide.
The three validation codes are not interchangeable — they carry different details payloads. See Error codes for each one and how to read it.
GET /booking/ returns only upcoming bookings by default
With no status parameter you get status=upcoming — which on a quiet account
is an empty list, even when the account has hundreds of past bookings. There is
no “all” value. To sweep everything, page each bucket separately:
400 — querystring/status must be equal to one of the allowed values.
Admin API
Some endpoints accept Application Admin API requests for elevated access. Only Application Admins hold an Admin API key; this is not extended to Team Admins.Reference
Explore every endpoint, with full schemas and a live “Try it” playground that runs againsthttps://api.cal.id, in the API Reference.
API reference and guides
Quickstart
From a fresh API key to a confirmed booking in five requests.
Error codes
Every
error.code the API returns, and what to do about each.Get your API key
Create, secure, and rotate the keys that authenticate your requests.
Rate limits
Headers, the 429 response, and how to back off cleanly.
Find your Event Type ID
Locate the numeric IDs your API calls need.
Create a booking
A step-by-step recipe from slots lookup to confirmed booking.