# Book or reschedule

`PUT /v1/leads/{lead_id}/appointment`

Books the lead's appointment at an open time, or moves its existing appointment there.

**Scope:** `leads:write`

**Access:** The key's member must also be allowed to create appointments (an admin, or a role with the Create appointments action).

A lead has one active appointment in its campaign: the first `PUT` books it and later `PUT`s move that same appointment (any other upcoming appointment the lead has there is cancelled). `start` must be a time the slots endpoint would list right now; whether a host has room is decided atomically at booking time.

Appointments that have started, completed or been handled are history and are never moved or overwritten: while the lead's current appointment is one of those the request fails with `409 appointment_not_movable` and nothing changes. A new time can be booked once that appointment started more than an hour ago.

What happens next is what happens for appointments the AI books: the team member is chosen by the campaign's routing (a moved appointment keeps its member when they have room), the lead's card moves to the appointment stage (a card is created if the lead has none) unless your team placed it in a stage automation may not change, the campaign's connected calendars, team notifications and appointment texts to the lead follow your settings, and the campaign's automated outreach stops while the appointment is active.

Retries are safe: sending the `start` the lead already holds answers `200` with `status: "unchanged"` and never books twice, even once that time is no longer listed.

The lead must belong to a campaign the key may reach and be one the key's member can see in DialEcho, otherwise `404`, checked before the member's permission to create appointments.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `lead_id` | `UUID` | Yes | The lead to book. |

## Body

**Content type:** `application/json`

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `start` | `timestamp` | Yes | A `start` from the campaign's open slots, or the same instant in any offset (`2026-11-02T14:30:00Z`). ISO 8601 with a UTC offset; a timestamp without one is rejected. |
| `notes` | `string` | No | Saved as the appointment's notes, shown on it in DialEcho's calendar. Omitted or blank keeps the stored notes. Up to 5,000 characters. |

## Response

**Status:** `201 Created · 200 OK`

`201` when a new appointment was booked; `200` when the lead already had an active appointment (same `appointment_id`). Responses never include the assigned team member or the lead's details.

| Name | Type | Description |
| --- | --- | --- |
| `appointment_id` | `UUID` | The appointment. |
| `lead_id` | `UUID` | The lead. |
| `start` | `timestamp` | The appointment's start in the campaign's appointment time zone, whatever offset you sent. |
| `status` | `string` | `booked`: a new appointment. `rescheduled`: the existing one was moved to `start`. `unchanged`: it was already at `start`. One of `booked`, `rescheduled`, `unchanged`. |

## Example request

**curl**

```bash
curl -X PUT "https://development.agencydevworks.ai/v1/leads/aaaaaaaa-0000-4000-8000-000000000001/appointment" \
  -H "Authorization: Bearer dei_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "start": "2026-11-02T09:30:00-05:00",
  "notes": "Prefers the side door."
}'
```

**JavaScript**

```js
const response = await fetch("https://development.agencydevworks.ai/v1/leads/aaaaaaaa-0000-4000-8000-000000000001/appointment", {
  method: "PUT",
  headers: {
    "Authorization": "Bearer dei_your_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "start": "2026-11-02T09:30:00-05:00",
    "notes": "Prefers the side door."
  }),
});
const data = await response.json();
```

## Example response

**Status:** `201 Created`

```json
{
  "appointment_id": "77777777-0000-4000-8000-000000000001",
  "lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
  "start": "2026-11-02T09:30:00-05:00",
  "status": "booked"
}
```

## Errors

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `leads:write`. |
| 403 | `permission_denied` | The key's member may not create appointments. Only for a lead they can see; any other lead is `404`. |
| 404 | `not_found` | The lead does not exist or the key cannot reach it. |
| 409 | `slot_unavailable` | The time is not open (any more): just taken, too soon, or not one of the campaign's slots. Nothing was booked or changed; re-fetch the slots. |
| 409 | `appointment_not_movable` | The lead's current appointment started less than an hour ago, or is completed or handled. Nothing was booked or changed. |
| 409 | `availability_not_configured` | The campaign has no appointment hours. |
| 409 | `no_hosts_configured` | No team member can take appointments in the campaign (none has an assignment weight above 0%). Fix its routing in DialEcho. |
| 413 | `payload_too_large` | The body is larger than 1 MB. It was not read and nothing was booked or changed. |
| 422 | `invalid_request` | `lead_id` is not a UUID, or the body is invalid: `start` missing or without a UTC offset, `notes` too long, or an unknown field. |

Every endpoint can also answer `401 invalid_api_key`, `429 rate_limited` and `503 dependency_unavailable`. See [Errors](/api/#errors) in the overview.

## Related endpoints

- `GET /v1/campaigns/{campaign_id}/appointment-slots` · [List open times](/api/appointments/list-slots)
- `POST /v1/leads` · [Create or update a lead](/api/leads/create)
- `GET /v1/leads/{lead_id}/conversion` · [Retrieve a conversion](/api/reporting/get-conversion)
