/v1/bookings
Read, create, cancel, reschedule, and patch bookings. This is the write-heavy
surface, and every write needs an
Idempotency-Key, and PATCH needs an
If-Match ETag.
A booking's UID is its UUID, and every endpoint that takes :uid rejects
non-UUIDs with 404 booking_not_found.
GET /v1/bookings
List bookings in the authenticated account.
| Method | GET |
| URL | https://api.42min.us/v1/bookings |
| Scope | bookings:read |
| Auth | Required |
Query parameters
| Param | Type | Notes |
|---|---|---|
limit | integer | Default 20, max 100. |
cursor | string | Opaque cursor from a prior meta.next_cursor. |
sort | string | start_at_desc (default), start_at_asc, created_at_desc, updated_at_asc, updated_at_desc. |
event_type_id | UUID | Filter to one event type. |
host_user_id | UUID | Filter to one host. |
attendee_email | string | Exact-match filter (case-sensitive). |
status | string | Comma-separated. Typical values: confirmed, canceled. |
start_date | ISO 8601 | Lower bound on start_at (inclusive). |
end_date | ISO 8601 | Upper bound on start_at (inclusive). |
updated_since | ISO 8601 | Only bookings whose updated_at is at or after this time. Pair with sort=updated_at_asc for catch-up reconciliation; see below. |
include_cancelled | boolean | When status isn't given, set false to exclude canceled. Defaults to including them. |
Response
{
"data": [
{
"uid": "01H…",
"version": 1,
"event_type_id": "01HXXX…",
"event_type_slug": "intro-call",
"title": "Intro call",
"status": "confirmed",
"start_at": "2026-05-20T10:00:00.000Z",
"end_at": "2026-05-20T10:30:00.000Z",
"timezone": "Europe/London",
"host": { "user_id": "01H…", "username": "ada", "email": "ada@example.com" },
"attendees": [{ "email": "bob@example.com", "name": "Bob Builder", "timezone": "Europe/Berlin" }],
"guests": [{ "email": "carol@example.com" }],
"location": { "type": "google_meet", "url": "https://meet.google.com/…", "value": null },
"metadata": { "crm_id": "C-7" },
"responses": null,
"calendar_sync_status": "synced",
"calendar_event_id": "abc123…",
"rescheduled_from_uid": null,
"cancelled_at": null,
"cancellation_reason": null,
"no_show_at": null,
"no_show_reason": null,
"created_at": "2026-05-14T09:00:00.000Z",
"updated_at": "2026-05-14T09:00:00.000Z"
}
],
"meta": { "request_id": "req_…", "next_cursor": null, "has_more": false }
}
Field notes:
responses(the booking-form answers) isnullon list responses, so fetch the detail to read them.calendar_sync_statusissynced/pending/failed/not_applicable(no calendar accounts attached).rescheduled_from_uidreferences the prior booking when this one is the result of a reschedule.
Catch-up reconciliation
If a webhook was paused (auto-paused after repeated
delivery failures, or paused by you) you'll have missed booking.* events.
To resync without guessing, sweep by modification time:
GET /v1/bookings?updated_since=<last-seen>&sort=updated_at_asc&limit=100
then follow meta.next_cursor to the end. updated_at_asc keeps the order
stable while you page. One caveat: an internal calendar-sync retry also bumps
updated_at, so a booking can resurface in this sweep with no user-facing
change, so make your reconciliation idempotent and that's harmless.
curl
curl -H "Authorization: Bearer $TOKEN" \
"https://api.42min.us/v1/bookings?status=confirmed&start_date=2026-05-01T00:00:00Z&limit=100"
# Catch-up after a webhook pause
curl -H "Authorization: Bearer $TOKEN" \
"https://api.42min.us/v1/bookings?updated_since=2026-05-18T00:00:00Z&sort=updated_at_asc&limit=100"
GET /v1/bookings/:uid
Return one booking with responses populated. Sets ETag for the version.
| Method | GET |
| URL | https://api.42min.us/v1/bookings/{uid} |
| Scope | bookings:read |
| Auth | Required |
Headers (response)
ETag: "3"
Response
Same shape as list items, with responses filled in:
{
"data": {
"uid": "01H…",
"version": 3,
"responses": { "phone": "+44 123 4567 8901", "company": "Acme" },
"…": "…"
},
"meta": { "request_id": "req_…" }
}
curl
curl -i -H "Authorization: Bearer $TOKEN" \
https://api.42min.us/v1/bookings/01H…
Common errors
404 booking_not_found: unknown UID, or UID is not a UUID (collapsed into the same 404 to avoid leaking format details).
POST /v1/bookings
Create a booking.
| Method | POST |
| URL | https://api.42min.us/v1/bookings |
| Scope | bookings:create |
| Auth | Required |
| Idempotency-Key | Required |
Headers
Authorization: Bearer <token>
Content-Type: application/json
Idempotency-Key: <unique-per-operation>
Body
Either event_type_id or (username + event_slug) is required.
{
"event_type_id": "01HXXX…",
"username": "ada",
"event_slug": "intro-call",
"start": "2026-05-20T10:00:00Z",
"timezone": "Europe/Berlin",
"attendee": {
"email": "bob@example.com",
"name": "Bob Builder",
"first_name": "Bob",
"last_name": "Builder",
"phone": "+49 30 12345678",
"timezone": "Europe/Berlin",
"sms_opt_in": false
},
"guests": [{ "email": "carol@example.com" }],
"responses": { "phone": "+49 30 12345678" },
"metadata": { "crm_id": "C-7" },
"utm_source": "web",
"utm_medium": "cta",
"utm_campaign": "spring-launch"
}
Field reference:
| Field | Type | Notes |
|---|---|---|
event_type_id | UUID | One of event_type_id or usernameevent_slug required. |
username / event_slug | string | Alternative to event_type_id. |
start | ISO 8601 | Required. Booking length is event_type.duration_minutes. |
timezone | IANA tz | Optional. Falls back to attendee.timezone, then UTC. |
attendee.email | string | Required. Max 254 chars, RFC-shaped. |
attendee.name | string | If omitted, built from first_name + last_name, else falls back to email. |
attendee.first_name / last_name | string | Max 127 each. |
attendee.phone | string | Max 64. |
attendee.sms_opt_in | boolean | Defaults to false. The attendee agrees to get text messages about this booking; workflow SMS steps text only attendees who agreed. Needs attendee.phone as a valid number in international format, starting with +, which is then stored in E.164 form. |
guests[] | array | Strings (emails) or {email, name?} objects. |
responses | object | Booking-form answers, keyed by question id. |
metadata | object | Free-form JSON. Stored alongside the booking; UTM params are merged in. |
utm_source / utm_medium / utm_campaign | string | Max 255 each. Merged into metadata. |
Response
201 Created with the full booking detail (same shape as GET /v1/bookings/:uid).
Initial version is 1.
curl
curl -X POST https://api.42min.us/v1/bookings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"event_type_id": "01HXXX…",
"start": "2026-05-20T10:00:00Z",
"timezone": "Europe/Berlin",
"attendee": { "email": "bob@example.com", "name": "Bob Builder" }
}'
Common errors
400 validation_error: required field missing or malformed.400 attendee_email_invalid: bad email format / length > 254.400 attendee_phone_required:attendee.sms_opt_inistruebutattendee.phoneis missing.400 attendee_phone_invalid:attendee.sms_opt_inistruebutattendee.phoneisn't a valid number in international format.400 missing_idempotency_key: header missing.404 event_type_not_found: bad id/slug.409 event_type_inactive: event type hasstatus != on.409 slot_in_past:startis in the past.409 slot_unavailable: slot is taken or otherwise blocked.409 idempotency_key_conflict: sameIdempotency-Keywas used with a different body in the last 24h.503 slot_lock_timeout: couldn't acquire the per-slot lock.Retry-After: 1.
POST /v1/bookings/:uid/cancel
Cancel a confirmed booking. Idempotent: calling it on an already-cancelled booking returns the current state, not an error.
| Method | POST |
| URL | https://api.42min.us/v1/bookings/{uid}/cancel |
| Scope | bookings:cancel |
| Auth | Required |
| Idempotency-Key | Required |
Body
{ "reason": "Schedule conflict" }
| Field | Type | Notes |
|---|---|---|
reason | string | Optional. Max 1024 chars. Surfaced in calendar removals and notifications. |
Response
200 OK with the updated booking. Version is bumped.
curl
curl -X POST https://api.42min.us/v1/bookings/01H…/cancel \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Schedule conflict"}'
Common errors
404 booking_not_found: unknown UID.409 booking_in_past: booking already started or finished.
POST /v1/bookings/:uid/reschedule
Move a confirmed booking to a new start time. The duration is preserved.
| Method | POST |
| URL | https://api.42min.us/v1/bookings/{uid}/reschedule |
| Scope | bookings:reschedule |
| Auth | Required |
| Idempotency-Key | Required |
Body
{
"start": "2026-05-22T15:00:00Z",
"timezone": "Europe/Berlin",
"reason": "Invitee requested a later time"
}
| Field | Type | Notes |
|---|---|---|
start | ISO 8601 | Required. Booking ends at start + duration_minutes. |
timezone | IANA tz | Optional. Falls back to the booking's current timezone. |
reason | string | Optional. Max 1024. |
Response
200 OK with the updated booking. Version is bumped.
curl
curl -X POST https://api.42min.us/v1/bookings/01H…/reschedule \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"start":"2026-05-22T15:00:00Z","timezone":"Europe/Berlin"}'
Common errors
400 validation_error:startmissing or unparseable.404 booking_not_found.409 booking_already_cancelled: booking isn'tconfirmed.409 booking_in_past: original start is in the past.409 slot_unavailable: new slot is taken.422 event_type_disallows_reschedule: the event type opts out of rescheduling.503 slot_lock_timeout: could not acquire the new-slot lock.
PATCH /v1/bookings/:uid
Patch a booking's mutable fields. Requires optimistic locking.
| Method | PATCH |
| URL | https://api.42min.us/v1/bookings/{uid} |
| Scope | bookings:update |
| Auth | Required |
| Idempotency-Key | Required |
| If-Match | Required (ETag from the most recent GET) |
Body
{
"metadata": { "stage": "qualified", "owner": "ada" },
"responses": { "phone": "+49 …" },
"attendee_name": "Bob D. Builder"
}
| Field | Type | Notes |
|---|---|---|
metadata | object | Shallow-merged into the existing metadata. Pass null for a key to clear it on your side and re-send. |
responses | object | Replaces the booking-form answers wholesale. |
attendee_name | string | Max 255 chars; stored as the primary attendee's name. |
Any other field returns 422 field_immutable with details.fields naming
the rejected keys. To change start use reschedule; to end the booking
use cancel.
Response
200 OK with the updated booking detail. Response headers include
ETag: "<new-version>".
Fires a webhook
A successful PATCH emits the booking.updated
webhook (subscribe with booking_updated).
The payload is the full booking plus a changed_fields array naming
exactly which of metadata / responses / attendee_name this call
changed. This is the only event that covers these silent edits; no
booking.rescheduled or booking.canceled fires for a PATCH.
curl
curl -X PATCH https://api.42min.us/v1/bookings/01H… \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "If-Match: \"3\"" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"metadata":{"stage":"qualified"},"attendee_name":"Bob D. Builder"}'
Common errors
400 validation_error:metadata/responsesmust be objects (not arrays/scalars);attendee_namemust be ≤ 255 chars.404 booking_not_found.409 version_conflict:If-Matchdoesn't match the currentversion. Re-GET and retry.422 field_immutable: body included a field that can't be patched.428 missing_if_match: header missing.
Last updated September 18, 2026.