---
title: "Create or update a booking from your system"
method: POST
path: "/api/projects/{id}/bookings"
tags: ["Bookings"]
---

# Create or update a booking from your system

`POST /api/projects/{id}/bookings`

## Request

- Base URL: `https://dosai.pro`
- URL: `https://dosai.pro/api/projects/{id}/bookings`
- Auth: HTTP bearer

One request per booking. Repeating the same `external_id` updates the booking instead of creating a second one;
a new `start_at` is a reschedule (reminders are re-planned, the confirmation is not sent again). `status: "cancelled"` cancels.

`start_at` without an offset (`2026-09-23 15:00`) is read in the project's time zone; the response returns it back as `start_at_local` so you can check how it was understood.
Messages follow the project's settings (Automations → Bookings) and go out between 9:00 and 21:00 project time.
The platform writes first only to contacts the WhatsApp number already knows. `contact` in the response is a hint, re-checked at send time: `known` (messages go out), `unknown` (never written to first, the booking is still kept), `unverified` (the check could not answer yet, the platform retries) or `instagram` (a 24h reply window, reminders do not fit).
Requires a key with the `write` scope.

## Request body

- BookingUpsert
  - `external_id` string, required: The booking's id in your system.
  - `phone` string, nullable: Client's WhatsApp number. Required unless `conversation_id` is given.
  - `conversation_id` string, uuid, nullable: A DOS AI dialog (any channel, including Telegram).
  - `client_name` string, nullable
  - `service` string, nullable
  - `start_at` string, required: ISO 8601 with offset, or `2026-09-23 15:00` / `23.09.2026 15:00` in the project's time zone.
  - `end_at` string, nullable
  - `status` 'scheduled' | 'cancelled'
  - `confirm` boolean: false: do not send the confirmation (your system sends its own). Reminders still go out.

## Response `200`

Existing booking updated, rescheduled or cancelled, wrapped in `data`.

- object
  - `data` BookingUpsertResult, required
    - `booking` Booking, required
      - `id` string, uuid, required
      - `external_id` string, nullable: Your id; null for bookings the assistant made in Google Calendar.
      - `source` 'calendar' | 'function' | 'api', required
      - `source_function` string, nullable
      - `status` 'scheduled' | 'cancelled', required
      - `client_name` string, nullable
      - `phone` string, nullable
      - `service` string, nullable
      - `channel` 'whatsapp' | 'telegram' | 'instagram' | 'null', nullable
      - `conversation_id` string, uuid, nullable
      - `start_at` string, date-time, required
      - `start_at_local` string, nullable: `start_at` in the project's time zone, with offset: check here how your time was understood.
      - `end_at` string, date-time, nullable
      - `end_at_local` string, nullable
      - `created_at` string, date-time
      - `messages` BookingMessage[], required
        - `id` string, uuid, required
        - `kind` 'confirmation' | 'reminder', required
        - `offset_minutes` integer, nullable: For a reminder: minutes before the visit.
        - `status` 'scheduled' | 'sent' | 'skipped' | 'failed' | 'cancelled', required
        - `send_at` string, date-time, required
        - `send_at_local` string, nullable: `send_at` in the project's time zone, with offset.
        - `sent_at` string, date-time, nullable
        - `skip_reason` string, nullable: Why it did not or will not go out: unknown_contact, instagram_window, night_skip, recent_conversation, after_due, past_due, booking_cancelled, channel_banned, previous_number, project_inactive, disabled, no_channel.
        - `body` string: The message text exactly as the client gets it.
    - `outcome` 'created' | 'updated' | 'rescheduled' | 'reopened' | 'cancelled', required
    - `contact` 'known' | 'unknown' | 'unverified' | 'instagram': Can the platform write to this client first: known = yes; unknown = never (they have not written to this WhatsApp number); unverified = the check could not answer, it is retried at send time; instagram = 24h reply window only. A hint at write time, re-checked before every send.
    - `timezone` string

## Other responses

- `201`: Booking created, wrapped in `data`.
- `400`: Validation failed.
- `401`: Missing, invalid, expired or revoked key.
- `403`: Not allowed. Distinguish by `code`: `INSUFFICIENT_SCOPE` (read-only key attempted a mutation), `API_KEY_PROJECT_SCOPE` (project-scoped key reached outside its project), `FORBIDDEN` (the user's role on this project is too low), `ACCOUNT_BANNED`.
- `404`: No such resource, or it belongs to another account.
- `429`: Too many requests. 120/min per key overall, plus a stricter per-endpoint budget. Retry after the number of seconds in Retry-After.

---

[API](https://skmtc.dev/dosai/apis/dos-ai-public-api.md) · [All operations](https://skmtc.dev/dosai/apis/dos-ai-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dosai/apis/dos-ai-public-api/revisions/38ce5f4e8f50?raw)
