POST /api/v1/bookings
Books a slot of a booking link for a prospect — a setter placing a call, a
script, or an agent. The closer is assigned by the link's rotation among its
free closers, exactly as on the booking page. The prospect is either a lead
saved earlier (leadId) or new details (contact).
If the prospect has an email address, they receive a confirmation email with its calendar file and token-based link to reschedule or cancel; the closer is notified.
| Role in the organization | This endpoint |
|---|---|
OWNER | Allowed |
ADMIN | Allowed |
MEMBER | Conditional — only with a business role below |
| Requirement | Rule for this endpoint |
|---|---|
| Business role | setter or closer, scoped to the organization |
| Resource scope | The school attached to the API key; the booking link must belong to it |
| Participation | None |
| Resource state | The link is active and configured; the slot is open |
| Header | Required | Description |
|---|---|---|
x-api-key | yes | API key created for this school |
Idempotency-Key | yes | Stable key for this booking intention |
X-Correlation-Id | no | Caller correlation id, returned or generated |
| Field | Type | Required | Nullable | Description |
|---|---|---|---|---|
bookingLink | string | yes | no | The booking link's address segment (linkSlug) |
leadId | UUID | conditional | no | A lead saved on this link; required when contact is absent |
contact.name | string | conditional | no | Required when leadId is absent |
contact.email | string | conditional | no | Email, or contact.phone |
contact.phoneCountry | string | no | no | Same codes as the lead endpoint; default +33 |
contact.phone | string | conditional | no | National number, or contact.email |
startsAt | ISO 8601 string | yes | no | Start of an open slot, exactly as listed |
timeZone | string | yes | no | IANA time zone of the prospect, used in their email |
locale | string | no | no | fr, en or es for the prospect's email; default fr |
organizationId is not a field — the school comes from the key.
{
"bookingLink": "appel-decouverte",
"contact": {"name": "Camille Martin", "email": "camille@example.com"},
"startsAt": "2026-10-05T07:15:00.000Z",
"timeZone": "Europe/Paris"
}curl -X POST "$COHORTE_API_BASE_URL/api/v1/bookings" \
-H "x-api-key: $COHORTE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{"bookingLink":"appel-decouverte","leadId":"'"$LEAD_ID"'","startsAt":"2026-10-05T07:15:00.000Z","timeZone":"Europe/Paris"}'201 Created — the call is booked; replayed is false.200 OK — a retry with the same Idempotency-Key; the original booking comes back with replayed: true, and no second email or notification is sent.400 Bad Request — missing Idempotency-Key, bookingLink, startsAt or timeZone, neither leadId nor contact, or invalid contact details; code is invalid_request.401 Unauthorized — missing, unknown or expired key; code is unauthorized.403 Forbidden — the bearer is not OWNER or ADMIN and holds no organization-scoped setter or closer responsibility; code is forbidden.404 Not Found — no active booking link with this address in the school, or a leadId that is not a lead of this link.409 Conflict — the slot is no longer open, or the lead already has an active booking; code is conflict.| Field | Type | Nullable | Description |
|---|---|---|---|
success | boolean | no | true for a successful request |
replayed | boolean | no | true when the same Idempotency-Key was already used |
data.id | UUID | no | The booking |
data.status | string | no | booked |
data.startsAt | ISO 8601 string | no | Call start, in UTC |
data.endsAt | ISO 8601 string | no | Call end, in UTC |
data.timeZone | string | no | The prospect's time zone |
data.bookingLink | string | yes | The booking link's address segment |
data.closer.name | string | no | The assigned closer |
data.prospect.name | string | no | The prospect's name |
data.prospect.email | string | yes | The prospect's email |
data.prospect.phone | string | yes | The prospect's phone, in international format |
correlationId | UUID | no | Correlation id for support |
{
"success": true,
"replayed": false,
"data": {
"id": "fd327d01-0000-4000-8000-000000000000",
"status": "booked",
"startsAt": "2026-10-05T07:15:00.000Z",
"endsAt": "2026-10-05T08:00:00.000Z",
"timeZone": "Europe/Paris",
"bookingLink": "appel-decouverte",
"closer": {"name": "Élodie Tanguy"},
"prospect": {"name": "Camille Martin", "email": "camille@example.com", "phone": null}
},
"correlationId": "33333333-0000-4000-8000-000000000000"
}GET /api/v1/booking-links/{orgSlug}/{linkSlug}/slots for the week.startsAt, shown in their time zone.POST /api/v1/bookings with the chosen startsAt and a stable
Idempotency-Key. On 409, list the slots again — never retry the same
startsAt blindly.Keep one idempotency key for one normalized intention. Current authorization is checked again on retry. A different payload or resource with that key, or an older key whose intention cannot be verified, returns 409 conflict without another mutation. Inspect the original outcome before creating a new command. Identical retries do not resend notifications.