Create a booking

Create a booking

POST/api/v1/bookings

scope bookings:writeIdempotent

Books a contact in. starts_at must be a time GET /bookings/slots currently offers for this type and member; the end time, title and location come from the type. If the time has gone — taken a moment ago, or no longer inside working hours — the answer is 409 conflict with reason: "slot_taken": load the slots again and let the customer choose another. Say who it is for in one of two ways. contact_id books an existing contact. contact takes a name and phone, matches a contact that already has that number, and creates one if there is none — the right choice for a website form, where you do not know whether this person has written in before. If the account has a booking message switched on in Bookings → Settings (a confirmation, a reminder, a cancellation notice), it is sent to the contact exactly as it would be for a booking made in the dashboard. Call it from your own server, not from browser JavaScript, and send an Idempotency-Key header so a retry does not book the customer twice.

Body

application/json
NameTypeDescription
type_idrequired
string (uuid)The booking type
assignee_user_idrequired
string (uuid)The member to book, from GET /bookable-members
starts_atrequired
string (date-time)A start time from GET /bookings/slots
contact_idoptional
string (uuid)An existing contact. Send this or `contact`
contactoptional
objectFind the contact by phone, or create it. Send this or `contact_id`
contact.namerequired
stringThe customer’s name
contact.phonerequired
stringE.164 phone number, e.g. +971501234567
contact.emailoptional
string (email) | null
notesoptional
string | nullFree text for the team
Request
curl -X POST "https://your-crm.example.com/api/v1/bookings" \
  -H "Authorization: Bearer $RAABTA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-3b4d-4c5e-8f7a-9b0c1d2e3f4a" \
  -d '{
    "type_id": "b00c1e00-1234-4abc-9def-000000000002",
    "assignee_user_id": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "starts_at": "2026-09-05T10:00:00.000Z",
    "contact": {
      "name": "Ayesha Khan",
      "phone": "+971501234567",
      "email": "ayesha@example.com"
    },
    "notes": "Wants to discuss a website redesign"
  }'
Response · 201 Created
{
  "data": {
    "id": "c0ffee00-1234-4abc-9def-000000000001",
    "contact_id": "9d7e4c11-5a2b-4f7c-8e3d-2a1b0c9d8e7f",
    "assignee_user_id": "2f3e4d5c-6b7a-4c8d-9e0f-1a2b3c4d5e6f",
    "type": {
      "id": "b00c1e00-1234-4abc-9def-000000000002",
      "name": "Consultation"
    },
    "title": "Consultation",
    "location": "Clinic, Room 2",
    "notes": "Wants to discuss a website redesign",
    "starts_at": "2026-09-05T10:00:00.000Z",
    "ends_at": "2026-09-05T10:30:00.000Z",
    "status": "scheduled",
    "source": "api",
    "cancel_reason": null,
    "completed_at": null,
    "created_at": "2026-09-02T10:05:00.000Z",
    "updated_at": "2026-09-02T10:05:00.000Z"
  }
}

Response

Wrapped in { data: … }
NameTypeDescription
idrequired
string (uuid)UUID
contact_idrequired
string (uuid) | nullThe contact this booking is for
assignee_user_idrequired
string (uuid) | nullThe member who owns the slot
typerequired
object | nullThe booking type, when one is set
type.idrequired
string (uuid)UUID
type.namerequired
string
titlerequired
string | nullSnapshotted from the type name at booking time
locationrequired
string | nullSnapshotted from the type at booking time
notesrequired
string | nullFree text recorded with the booking
starts_atrequired
string (date-time)ISO 8601 timestamp
ends_atrequired
string (date-time)ISO 8601 timestamp
statusrequired
string (enum)Where the booking is in its lifescheduledconfirmedcompletedno_showcancelled
sourcerequired
string (enum)Who booked it: staff in the dashboard, the AI assistant, the public booking page, or this APIstaffaipublicapi
cancel_reasonrequired
string | nullWhat was recorded when it was cancelled
completed_atrequired
string (date-time) | nullWhen it was marked completed
created_atrequired
string (date-time)ISO 8601 timestamp
updated_atrequired
string (date-time)ISO 8601 timestamp

Errors

StatusCodeWhen
400bad_requestThe request could not be parsed: malformed JSON, an invalid cursor, or a query parameter of the wrong shape.
400validation_errorThe body or query failed validation. `details` lists each failing field with a `path` and a `message`.
401unauthorizedNo usable API key: the Authorization header is missing or malformed, or the key is unknown, revoked or expired. The three are deliberately indistinguishable.
402plan_limit_reachedThe account has reached a plan limit for this resource. `details[0].limit` is the cap; ask the account owner to raise it.
403forbiddenThe key is valid but lacks the scope this endpoint requires, or the request came from an address outside the key’s IP allowlist. The message says which.
403account_suspendedThe account this key belongs to is suspended. Rotating the key will not help; contact support.
409conflictThe request conflicts with current state: a duplicate phone number on create, an invalid state transition, or an idempotent request that is still in flight.
422unprocessableThe request was well-formed but cannot be carried out. `reason` is a stable string saying why (for example `outside_window` or `stage_not_in_pipeline`).
429rate_limitedThe per-key budget, or the per-IP budget for failed authentication, is exhausted. Honour `Retry-After` before retrying.
500internalSomething failed on our side. Safe to retry with the same Idempotency-Key; quote `request_id` if it persists.