# API reference

The complete endpoint list, generated from the same registry that builds the OpenAPI document —
so it cannot describe an endpoint that does not exist, or miss one that does.

The machine-readable spec is at [`/developers/openapi.json`](/developers/openapi.json).

## Meta

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/ping` | Verify a credential | _any valid credential_ |

**`GET /ping`** — Echoes back which workspace and scopes your credential resolves to. The first call every integration should make.
## Contacts

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/contacts` | List contacts | `contacts:read` |
| `POST` | `/api/v1/contacts` | Create a contact | `contacts:write` |
| `GET` | `/api/v1/contacts/{id}` | Retrieve a contact | `contacts:read` |
| `PATCH` | `/api/v1/contacts/{id}` | Update a contact | `contacts:write` |
| `DELETE` | `/api/v1/contacts/{id}` | Erase a contact's personal data | `contacts:write` |
| `GET` | `/api/v1/contacts/{id}/events` | List a contact's timeline | `events:read` |
| `GET` | `/api/v1/contacts/{id}/notes` | List a contact's notes | `events:read` |
| `POST` | `/api/v1/contacts/{id}/notes` | Add a note | `contacts:write` |

**`DELETE /contacts/{id}`** — ANONYMIZES in place — it does not delete the row. A contact is referenced by deals, commissions and messages a brokerage is required to keep, so a true delete would either cascade those away or leave them dangling. Names, addresses, message bodies and activity titles are scrubbed; the structural records survive pointing at an anonymized contact.

**`POST /contacts/{id}/notes`** — Adding a note pauses the contact's AI auto-response, exactly as it does in the product — a human (or an integration) touching a conversation is the signal that the AI should stop talking over them.
## Leads

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/leads` | List leads | `leads:read` |
| `POST` | `/api/v1/leads` | Create and route a lead | `leads:write` |
| `GET` | `/api/v1/leads/{id}` | Retrieve a lead | `leads:read` |
| `POST` | `/api/v1/leads/{id}/assign` | Route or re-route a lead | `leads:write` |
| `POST` | `/api/v1/leads/routing-preview` | Preview where a lead would be routed | `leads:read` |

**`POST /leads`** — Creates the lead and routes it through the assignment engine unless `skip_routing` is set. A lead created WITHOUT a `consent` object carries no consent artifact, so the pre-dial and pre-send gates will refuse AI outbound to it — the response says so in `ai_outbound_allowed`.

**`POST /leads/routing-preview`** — A dry run: creates nothing, assigns nothing, notifies nobody. Returns the rule that would win, the assignee, and the full considered set with the failing conditions spelled out. For round-robin methods the rotation cursor is NOT advanced, so the live pick may be a different member of the same pool — `reason` says so.
## Messaging

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/messages` | List messages | `messages:read` |
| `POST` | `/api/v1/messages` | Send a text, WhatsApp message or email | `messages:send` |

**`POST /messages`** — Routes through the same send path the product uses, so every compliance check applies: suppression, opt-out, A2P registration, TCPA quiet hours and consent. A refused send returns **422 `compliance_blocked`** with the reason — never a silent success — and the attempt is still recorded in the workspace's inbox.
## Calls

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/calls` | List calls | `calls:read` |
| `GET` | `/api/v1/calls/{id}` | Retrieve a call with transcript and recording | `calls:read` |

**`GET /calls`** — Includes AI-handled calls with their summaries. `handled_by` distinguishes AI from human.

**`GET /calls/{id}`** — `recording_url` is a five-minute signed link to our own proxy, never the carrier's URL (which is credential-protected and would 401 for you).
## Deals

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/deals` | List deals | `deals:read` |
| `POST` | `/api/v1/deals` | Create a deal | `deals:write` |
| `GET` | `/api/v1/deals/{id}` | Retrieve a deal | `deals:read` |
| `PATCH` | `/api/v1/deals/{id}` | Update a deal | `deals:write` |
| `GET` | `/api/v1/pipelines` | List pipelines and their stages | `deals:read` |
## Listings

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/listings` | List the workspace's own listings | `listings:read` |

**`GET /listings`** — These are listings the workspace authored, NOT MLS inventory. MLS data is governed by per-board display rules that a general-purpose REST feed cannot honour.
## Calendar

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/appointments` | List appointments | `appointments:read` |
| `POST` | `/api/v1/appointments` | Book an appointment | `appointments:write` |
| `GET` | `/api/v1/appointments/{id}` | Retrieve an appointment | `appointments:read` |
| `PATCH` | `/api/v1/appointments/{id}` | Reschedule an appointment or record its outcome | `appointments:write` |
| `GET` | `/api/v1/tasks` | List tasks | `appointments:read` |
| `POST` | `/api/v1/tasks` | Create a follow-up task | `appointments:write` |

**`POST /appointments`** — Pushes the event out to the creator's connected calendar when one exists, exactly as an in-product booking does.

**`PATCH /appointments/{id}`** — Send `starts_at` AND `ends_at` together to move it; send `outcome` to record what happened. Both are separate domain writes, and moving with only one end is refused.

**`POST /tasks`** — Deduped within 60 seconds, so a retried webhook does not produce two identical follow-ups. `created: false` means an identical task already existed.
## Automations

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/automations` | List automations | `automations:read` |
| `POST` | `/api/v1/automations/{id}/enroll` | Start an automation for a contact | `automations:manage` |
| `GET` | `/api/v1/personas` | List AI personas | `automations:read` |
| `GET` | `/api/v1/campaigns` | List outbound campaigns | `automations:read` |

**`POST /automations/{id}/enroll`** — Returns **202**: the enrolment is durable immediately, and the automation runs on the next engine tick. Enrolling into a disabled automation is refused rather than silently dropped.
## Webhooks

| Method | Path | Summary | Scope |
|---|---|---|---|
| `GET` | `/api/v1/webhooks` | List webhook subscriptions | `webhooks:manage` |
| `POST` | `/api/v1/webhooks` | Subscribe to events | `webhooks:manage` |
| `DELETE` | `/api/v1/webhooks/{id}` | Unsubscribe | `webhooks:manage` |

**`POST /webhooks`** — The signing secret is returned exactly once, here. Every delivery carries `X-Webhook-Signature: sha256=<hmac>` over the exact request body.
