# Contacts

> Every field of /v1/contacts: create a contact, find or search for one, read, change, tag, opt out and delete it, its phone numbers and emails, and the custom contact fields.

<!-- Generated by apps/docs/scripts/gen-reference.mjs from apps/docs/public/openapi/v1.json. Do not edit: change the API's schemas (then `pnpm --filter @seasalt/api openapi:emit`) or the notes in apps/docs/src/reference-notes/, then run `pnpm --filter @seasalt/docs gen:reference`. -->

The people you text and call: their phone number, email, name, company, attributes, tags and opt-outs, and the workspace's custom contact fields. How-to: [Keep contacts](/platform-api/contacts/).

Generated from [`/openapi/v1.json`](/openapi/v1.json); interactive version: [API reference](/openapi/v1/).

## Endpoints

| method | path | what it does | success |
|---|---|---|---|
| [`GET`](#get-v1contacts) | `/v1/contacts` | List contacts, find the one with a phone number or email, search, or sync changes | `200` |
| [`POST`](#post-v1contacts) | `/v1/contacts` | Create a contact | `201` |
| [`GET`](#get-v1contactsid) | `/v1/contacts/{id}` | Read a contact | `200` |
| [`PATCH`](#patch-v1contactsid) | `/v1/contacts/{id}` | Change a contact | `200` |
| [`DELETE`](#delete-v1contactsid) | `/v1/contacts/{id}` | Delete a contact | `200` |
| [`POST`](#post-v1contactsidtags) | `/v1/contacts/{id}/tags` | Tag a contact | `200` |
| [`DELETE`](#delete-v1contactsidtagstag) | `/v1/contacts/{id}/tags/{tag}` | Untag a contact | `200` |
| [`POST`](#post-v1contactsiddnc) | `/v1/contacts/{id}/dnc` | Opt a contact out, or back in | `200` |
| [`GET`](#get-v1contactsidconsent-events) | `/v1/contacts/{id}/consent-events` | List a contact's consent record | `200` |
| [`POST`](#post-v1contactsidconsent-events) | `/v1/contacts/{id}/consent-events` | Record consent, or its withdrawal, by hand | `201` |
| [`GET`](#get-v1contactsidchannels) | `/v1/contacts/{id}/channels` | List a contact's phone numbers and emails | `200` |
| [`POST`](#post-v1contactsidchannels) | `/v1/contacts/{id}/channels` | Add a phone number or email to a contact | `200`, `201` |
| [`DELETE`](#delete-v1contactsidchannelsaddress) | `/v1/contacts/{id}/channels/{address}` | Remove a phone number or email from a contact | `200` |
| [`GET`](#get-v1contact-fields) | `/v1/contact-fields` | List the custom contact fields | `200` |

## `GET /v1/contacts`

List contacts, find the one with a phone number or email, search, or sync changes.

Without a filter: every contact, newest first. With `phone` or `email`: the contacts that have it, the one it belongs to first (a number can be shared by two people); with both, the contacts that have both. With `query`: the contacts whose name, company, tags, notes, numbers or emails contain that text, newest first. `tag`, `attribute_key` / `attribute_value` and `updated_after` filter the list exactly, combine with each other and with `query`, and not with `phone` or `email`. With `updated_after` the order is oldest change first, by `updated_at` then `id`: a contact that changes while you page moves to the end and is answered again (keep the latest by `id`). Deleted contacts are left out, and a sync does not see deletions.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `limit` | query | integer<br>1–200 | Page size, 1 to 200 (default 50). |
| `after` | query | string | The previous page's `next_cursor`. |
| `phone` | query | string | E.164 (in a query string, send the `+` as `%2B`). Matches the number on any phone channel: SMS, calls, WhatsApp. |
| `email` | query | string | An email (any case). |
| `query` | query | string | Up to 100 characters to search for, in any case: part of a name, a company, a tag, a number, an email or the contact's notes. At least 3 once lower-cased; a Chinese name counts by its pinyin. Not with `phone` or `email`. |
| `tag` | query | string<br>1–64 chars | Only contacts that carry exactly this tag: case-sensitive, as stored (`vip` is not `VIP` or `vip-lapsed`). One tag per request; 1 to 64 characters after trimming. |
| `attribute_key` | query | string<br>1–64 chars | Only contacts whose `attributes` has this key (1 to 64 characters). With `attribute_value`, only those whose value for it is that value. |
| `attribute_value` | query | string<br>≤ 500 chars | With `attribute_key`: the value, as text, exactly as the contact object prints it (`gold`, `5`, `true`). Up to 500 characters. Without `attribute_key` it is `400 invalid_request`. |
| `updated_after` | query | string | ISO 8601 time. Only contacts whose `updated_at` is at or after it, oldest change first (by `updated_at`, then `id`). For incremental sync: pass the largest `updated_at` you processed minus 2 minutes, page to the end, and upsert by `id`. `updated_at` moves when a contact's fields, tags, attributes, numbers or emails (added, changed or removed) or opt-outs change. |
| `include_total` | query | string<br>`true`, `false` | `true` adds `total`: how many contacts the list holds with these filters, all pages together (counted once, on this request). Default `false`. |

</div>

**Answers** `200` with [ContactPage](#contactpage). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `POST /v1/contacts`

Create a contact.

Makes one contact with a `phone`, an `email`, or both. The phone is stored for SMS, calls and WhatsApp alike; that is not consent: opt-outs stay on the number, and `dnc` shows them. One contact per number: a phone or email another contact already has is `409 contact_exists`, naming that contact.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `idempotency-key` | header | string<br>≤ 255 chars | Send one with every create. A retry with the same key makes no second contact: it answers the first one as it is now. Rules: [Idempotency](/platform-api/retries/#idempotency). |

</div>

**Request body:** [ContactCreate](#contactcreate).

**Answers** `201` with [Contact](#contact). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `GET /v1/contacts/{id}`

Read a contact.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Answers** `200` with [Contact](#contact). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `PATCH /v1/contacts/{id}`

Change a contact.

Only the fields you send change. A new `phone` replaces the old number on every phone channel, and the old number passes to the next person who shares it, if anyone does. `attributes` merge: `null` removes a key. Tags and opt-outs have routes of their own.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Request body:** [ContactUpdate](#contactupdate).

**Answers** `200` with [Contact](#contact). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `DELETE /v1/contacts/{id}`

Delete a contact.

The contact disappears from `/v1/contacts`, lists and future campaigns; past messages, calls and campaign records still show it. A number or email another contact shares passes to that contact. Opt-outs stay on the number or email: a deleted contact never makes an opted-out number reachable.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Answers** `200` with the object below. Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required |   |
| `object` | string, required<br>always `contact` |   |
| `deleted` | boolean, required<br>always `true` |   |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

## `POST /v1/contacts/{id}/tags`

Tag a contact.

Adds `tags` to the contact; a tag it already has stays once. A workflow that starts when a tag is applied starts for each new one. Answers the contact.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Request body:** [ContactTagsAdd](#contacttagsadd).

**Answers** `200` with [Contact](#contact). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `DELETE /v1/contacts/{id}/tags/{tag}`

Untag a contact.

Removes one tag (URL-encoded) from the contact. A tag it does not have changes nothing and answers the same. Answers the contact.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |
| `tag` | path | string, required | The tag, URL-encoded. |

</div>

**Answers** `200` with [Contact](#contact). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `POST /v1/contacts/{id}/dnc`

Opt a contact out, or back in.

Opts every phone number of the contact out of texts, calls or WhatsApp, or its email out of email (`status: opted_out`), or back in (`opted_in`). Opt-outs are kept on the number or email, so they hold for anyone who has it. An opt-in never clears an opt-out the person sent themselves: that is `409 stopped_by_person`, and nothing changes. All channels change, or none.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Request body:** [ContactOptOutUpdate](#contactoptoutupdate).

**Answers** `200` with [ContactWithOptOut](#contactwithoptout). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `GET /v1/contacts/{id}/consent-events`

List a contact's consent record.

The consent events of the contact's phone numbers and emails, newest first: the first-contact notice (`notice_shown`, with the text exactly as sent), consent deemed given, STOP (`withdrawn`) and START (`restored`), and events recorded by hand. Kept on the number or email, like opt-outs. A record, not a send rule: `dnc` decides what may be sent. Omni Octo records it; it does not decide whether a notice is legally sufficient.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |
| `limit` | query | integer<br>1–200 | How many, 1 to 200 (default 50), newest first. |

</div>

**Answers** `200` with [ConsentEventList](#consenteventlist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `POST /v1/contacts/{id}/consent-events`

Record consent, or its withdrawal, by hand.

Adds an event to the contact's consent record: `recorded_by_operator` (you have consent from elsewhere: a signed form, a call) or `withdrawn`. For `channel` `sms`, `voice` or `whatsapp` it is recorded on the contact's phone number, for `email` on its email. `withdrawn` also opts that channel out, as `POST /v1/contacts/{id}/dnc` does (every number, or every email, of the contact), in the same transaction: both are saved, or neither. Opting back in is `POST /v1/contacts/{id}/dnc` (a STOP the person sent is cleared only by their START).

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Request body:** [ConsentEventCreate](#consenteventcreate).

**Answers** `201` with [ConsentEvent](#consentevent). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `GET /v1/contacts/{id}/channels`

List a contact's phone numbers and emails.

Every phone number and email of the contact, one item each (a number's SMS, calls and WhatsApp are one item): phones before emails; in each, the primary one first (the one the contact's `phone` or `email` shows), then oldest first. Each with its opt-outs and whether another contact shares it.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |
| `limit` | query | integer<br>1–200 | Page size, 1 to 200 (default 100). |
| `after` | query | string | The previous page's `next_cursor`. |

</div>

**Answers** `200` with [ContactChannelList](#contactchannellist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `POST /v1/contacts/{id}/channels`

Add a phone number or email to a contact.

Adds one `phone` (for SMS, calls and WhatsApp) or one `email` beside the ones the contact has; `PATCH` `phone` would replace them all. One contact per number: one another contact has is `409 contact_exists`, and nothing changes. Two people who share a number are set up in the dashboard.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |

</div>

**Request body:** [ContactChannelCreate](#contactchannelcreate).

**Answers** `200` with [ContactChannel](#contactchannel); `201` with [ContactChannel](#contactchannel). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## `DELETE /v1/contacts/{id}/channels/{address}`

Remove a phone number or email from a contact.

`address` is the phone number in E.164 (URL-encode the `+` as `%2B`) or the email. A number goes from SMS, calls and WhatsApp at once. Its opt-outs stay on the number, and someone who shares it keeps it. A contact keeps at least one phone number or email.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The contact's `ct-…` id or UUID. |
| `address` | path | string, required | E.164 (`%2B15555550100`) or an email. |

</div>

**Answers** `200` with the object below. Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `contact_channel` |   |
| `address` | string, required |   |
| `deleted` | boolean, required<br>always `true` |   |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

## `GET /v1/contact-fields`

List the custom contact fields.

The workspace's custom contact fields, in the dashboard's order: each one's `key` is a key of a contact's `attributes` and a campaign's `{{contact.<key>}}`. Fields are defined in the dashboard; any key works in `attributes`, defined or not.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `limit` | query | integer<br>1–200 | Page size, 1 to 200 (default 50). |
| `after` | query | string | The previous page's `next_cursor`. |

</div>

**Answers** `200` with [ContactFieldList](#contactfieldlist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [contacts](/platform-api/errors/#contacts), [lists](/platform-api/errors/#lists).

## Objects

### ContactPage

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [Contact](#contact), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |
| `total` | integer<br>≥ 0 | Only with `include_total=true`: how many contacts match these filters, all pages together, counted up to 100,000. |
| `total_capped` | boolean<br>always `true` | Present (`true`) when more than 100,000 contacts match: `total` is then 100000 and the real number is larger. |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### Contact

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | `ct-…`. |
| `object` | string, required<br>always `contact` |   |
| `uuid` | string, required |   |
| `name` | string or null, required |   |
| `phone` | string or null, required | E.164, or null for none. |
| `email` | string or null, required |   |
| `company` | string or null, required |   |
| `attributes` | map of string to string, required |   |
| `tags` | array of string, required | The contact's tags, in the order they were added. |
| `dnc` | object, required | Opt-out state, kept on the number or email (not on the contact). Change it with `POST /v1/contacts/{id}/dnc` or in the dashboard; a person's STOP is cleared only by their START. |
| `dnc.sms` | boolean or null, required | `true` when the contact's phone number, for texts is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `dnc.voice` | boolean or null, required | `true` when the contact's phone number, for calls is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `dnc.whatsapp` | boolean or null, required | `true` when the contact's phone number, for WhatsApp is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `dnc.email` | boolean or null, required | `true` when the contact's email is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `consent` | [ContactConsentStatus](#contactconsentstatus) or null, required |   |
| `consents` | array of [ContactConsentStatus](#contactconsentstatus) or null, required | The latest consent event of each of the contact's numbers and emails, newest first (at most 20). |
| `created_at` | string or null, required |   |
| `updated_at` | string or null, required | When the contact last changed: its fields, tags, phone numbers and emails (added, removed, made primary), or its opt-outs through `/v1`. |

</div>

Example: see [Create a contact](/platform-api/contacts/#create-a-contact).

### ContactCreate

`phone`, `email`, or both.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `phone` | string<br>pattern `^\+[1-9]\d{6,14}$` | E.164 (`+14155550100`). Stored for SMS, calls and WhatsApp alike (one contact per number); that is not consent: opt-outs stay on the number. A number another contact already has is `409 contact_exists`. |
| `email` | string<br>≤ 254 chars | An email another contact already has is `409 contact_exists`. |
| `name` | string<br>≤ 200 chars |   |
| `company` | string<br>≤ 200 chars |   |
| `attributes` | map of string to string or number or boolean<br>values ≤ 500 chars | Custom fields, readable as `{{contact.<key>}}` in a campaign's `variables`. Values are stored as text. Up to 20 keys (64 characters each), values up to 500 characters, 2048 bytes in all. |
| `tags` | array of string<br>≤ 50 items; each 1–64 chars | Up to 50 tags, 64 characters each. A workflow that starts on a tag starts for these too. |

</div>

```json
{
  "tags": [
    "vip"
  ]
}
```

### ContactUpdate

Only the fields you send change.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `phone` | string or null<br>pattern `^\+[1-9]\d{6,14}$` | A new number replaces the old one on every phone channel; `null` removes it. A number another contact has is `409 contact_exists`, and nothing changes (the whole PATCH is one transaction). An opt-out on the old number stays on that number. |
| `email` | string or null<br>≤ 254 chars | A new email replaces the old one; `null` removes it. |
| `name` | string or null<br>≤ 200 chars |   |
| `company` | string or null<br>≤ 200 chars |   |
| `attributes` | map of string to string or number or boolean or null<br>values ≤ 500 chars | Merged into the contact's attributes: a value sets that key, `null` removes it, keys you leave out stay. Up to 20 keys (64 characters each), values up to 500 characters, 2048 bytes in all. |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### ContactTagsAdd

<div class="ref-table">

| field | type | description |
|---|---|---|
| `tags` | array of string, required<br>1–50 items; each 1–64 chars | 1 to 50 tags, each trimmed and 1 to 64 characters. A tag the contact already has stays once. |

</div>

```json
{
  "tags": [
    "vip"
  ]
}
```

### ContactOptOutUpdate

<div class="ref-table">

| field | type | description |
|---|---|---|
| `channels` | array of string, required<br>1–4 items; each `sms`, `voice`, `whatsapp`, `email` | Which channels: `sms` (texts), `voice` (calls) and `whatsapp` apply to every phone number of the contact, `email` to every email. |
| `status` | string, required<br>`opted_out`, `opted_in` | `opted_out`: never text, call or email it there. `opted_in`: reachable again, unless the person sent STOP themselves (`409 stopped_by_person`). |
| `reason` | string<br>≤ 500 chars | Why, for the opt-out history in the dashboard. |

</div>

```json
{
  "channels": [
    "sms"
  ],
  "status": "opted_out",
  "reason": "Asked on the phone not to be called"
}
```

### ContactWithOptOut

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | `ct-…`. |
| `object` | string, required<br>always `contact` |   |
| `uuid` | string, required |   |
| `name` | string or null, required |   |
| `phone` | string or null, required | E.164, or null for none. |
| `email` | string or null, required |   |
| `company` | string or null, required |   |
| `attributes` | map of string to string, required |   |
| `tags` | array of string, required | The contact's tags, in the order they were added. |
| `dnc` | object, required | Opt-out state, kept on the number or email (not on the contact). Change it with `POST /v1/contacts/{id}/dnc` or in the dashboard; a person's STOP is cleared only by their START. |
| `dnc.sms` | boolean or null, required | `true` when the contact's phone number, for texts is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `dnc.voice` | boolean or null, required | `true` when the contact's phone number, for calls is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `dnc.whatsapp` | boolean or null, required | `true` when the contact's phone number, for WhatsApp is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `dnc.email` | boolean or null, required | `true` when the contact's email is opted out (by anyone who has it, on any record); `null` when the contact has none. |
| `consent` | [ContactConsentStatus](#contactconsentstatus) or null, required |   |
| `consents` | array of [ContactConsentStatus](#contactconsentstatus) or null, required | The latest consent event of each of the contact's numbers and emails, newest first (at most 20). |
| `created_at` | string or null, required |   |
| `updated_at` | string or null, required | When the contact last changed: its fields, tags, phone numbers and emails (added, removed, made primary), or its opt-outs through `/v1`. |
| `opt_out` | object, required | What this request changed. |
| `opt_out.status` | string, required<br>`opted_out`, `opted_in` |   |
| `opt_out.applied` | array of string, required<br>each `sms`, `voice`, `whatsapp`, `email` | The channels now in `status`. |
| `opt_out.kept_opted_out` | array of string, required<br>each `sms`, `voice`, `whatsapp`, `email` | For an opt-in: channels that stay opted out, because another contact who has the same number sent STOP from it. |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### ContactConsentStatus

The latest consent event across the contact's phone numbers and emails (the first-contact notice, consent deemed given, STOP / START, or recorded by hand), or `null` when there is none. Omni Octo records it; it does not decide whether it is legally sufficient. History: `GET /v1/contacts/{id}/consent-events`.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `status` | string, required<br>`notice_shown`, `notice_unconfirmed`, `deemed_given`, `withdrawn`, `restored`, `recorded` | `notice_shown` (the first-contact notice went out), `notice_unconfirmed` (a notice send whose outcome is unknown), `deemed_given` (consent deemed given: the person wrote again after the notice, or the notice went out, per the agent's setting), `withdrawn` (STOP, or recorded by hand), `restored` (START after a STOP), `recorded` (recorded by hand). A record, not a send rule: opt-outs (`dnc`) decide what may be sent. |
| `at` | string, required | When (ISO 8601). |
| `channel` | string, required<br>`sms`, `voice`, `whatsapp`, `email` |   |
| `notice_version` | integer or null, required | The version of the agent's notice the event is about; `null` for a STOP / START or a hand-recorded event. |
| `agent` | string or null, required | The agent (`ag-…`) whose notice it was, or `null`. |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### ConsentEventList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [ConsentEvent](#consentevent), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### ConsentEvent

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | The event's id (UUID). |
| `object` | string, required<br>always `consent_event` |   |
| `channel` | string, required<br>`sms`, `voice`, `whatsapp`, `email` |   |
| `address` | string, required | The phone number (E.164) or email it is about. |
| `kind` | string, required<br>`notice_shown`, `deemed_given`, `withdrawn`, `restored`, `recorded_by_operator` | What happened. |
| `basis` | string, required<br>`first_contact_notice`, `webchat_prechat`, `opt_out_keyword`, `operator`, `api` | Why the event exists. |
| `status` | string or null, required<br>`notice_shown`, `notice_unconfirmed`, `deemed_given`, `withdrawn`, `restored`, `recorded` | The status this event puts the number in: `notice_shown` (the first-contact notice went out), `notice_unconfirmed` (a notice send whose outcome is unknown), `deemed_given` (consent deemed given: the person wrote again after the notice, or the notice went out, per the agent's setting), `withdrawn` (STOP, or recorded by hand), `restored` (START after a STOP), `recorded` (recorded by hand). A record, not a send rule: opt-outs (`dnc`) decide what may be sent. `null` for a notice send that failed (it shows nothing). |
| `delivery_status` | string or null, required<br>`pending`, `sent`, `unconfirmed`, `failed` | Only on `notice_shown`: whether the notice went out. |
| `agent` | string or null, required | `ag-…`, or `null`. |
| `notice_version` | integer or null, required |   |
| `notice_text` | string or null, required | The notice exactly as sent (a snapshot: a later edit never changes it). |
| `notice_sha256` | string or null, required | sha256 (hex) of `notice_text`. |
| `note` | string or null, required |   |
| `source` | string or null, required | `webhook`, `dashboard`, `api` or `octo`. |
| `occurred_at` | string, required |   |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### ConsentEventCreate

<div class="ref-table">

| field | type | description |
|---|---|---|
| `kind` | string, required<br>`recorded_by_operator`, `withdrawn` | `recorded_by_operator`: you have consent from elsewhere (a signed form, a call). `withdrawn`: the person withdrew it; this also opts that channel out (as `POST /v1/contacts/{id}/dnc`). |
| `channel` | string, required<br>`sms`, `voice`, `whatsapp`, `email` | Which of the contact's addresses: `sms`, `voice` and `whatsapp` mean its phone number, `email` its email. |
| `note` | string<br>≤ 500 chars | Why, or where the consent came from. |

</div>

```json
{
  "kind": "recorded_by_operator",
  "channel": "sms",
  "note": "Signed the clinic's paper form on 2026-10-30"
}
```

### ContactChannelList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [ContactChannel](#contactchannel), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

```json
{
  "object": "list",
  "data": [
    {
      "object": "contact_channel",
      "type": "phone",
      "address": "+15555550100",
      "channels": [
        "sms"
      ],
      "primary": false,
      "shared": false,
      "dnc": {
        "sms": null,
        "voice": null,
        "whatsapp": null,
        "email": null
      },
      "created_at": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### ContactChannel

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `contact_channel` |   |
| `type` | string, required<br>`phone`, `email` |   |
| `address` | string, required | E.164, or the email. |
| `channels` | array of string, required<br>each `sms`, `voice`, `whatsapp`, `email` | The channels the contact has it on. A phone added here has all three. |
| `primary` | boolean, required | The contact's primary phone number or email: the one its `phone` (or `email`) shows. A contact has one primary phone number and one primary email. |
| `shared` | boolean, required | Another contact has it too (two people set up in the dashboard as sharing it). |
| `dnc` | object, required | The opt-outs kept on this number or email. |
| `dnc.sms` | boolean or null, required | Opted out of texts; null when it does not apply. |
| `dnc.voice` | boolean or null, required | Opted out of calls; null when it does not apply. |
| `dnc.whatsapp` | boolean or null, required | Opted out of WhatsApp; null when it does not apply. |
| `dnc.email` | boolean or null, required | Opted out of email; null when it does not apply. |
| `created_at` | string or null, required |   |

</div>

```json
{
  "object": "contact_channel",
  "type": "phone",
  "address": "+15555550100",
  "channels": [
    "sms"
  ],
  "primary": false,
  "shared": false,
  "dnc": {
    "sms": null,
    "voice": null,
    "whatsapp": null,
    "email": null
  },
  "created_at": null
}
```

### ContactChannelCreate

`phone` or `email`, one per request.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `phone` | string<br>pattern `^\+[1-9]\d{6,14}$` | E.164 (`+15555550100`). Stored for SMS, calls and WhatsApp alike. A number another contact has is `409 contact_exists`. |
| `email` | string<br>≤ 254 chars | An email another contact has is `409 contact_exists`. |
| `primary` | boolean | `true` makes it the contact's primary phone number (or email): the one its `phone` (or `email`) shows, and the one a campaign uses. The other kind's primary stays as it is. |

</div>

Example: see [Keep contacts](/platform-api/contacts/).

### ContactFieldList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [ContactField](#contactfield), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; `null` on the last. |

</div>

```json
{
  "object": "list",
  "data": [
    {
      "id": "fd-0000000001",
      "object": "contact_field",
      "key": "tier",
      "name": "Tier",
      "type": "text",
      "options": [],
      "position": 1,
      "agent_access": "hidden",
      "agent_role": false
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### ContactField

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | `fd-…`. |
| `object` | string, required<br>always `contact_field` |   |
| `key` | string, required | The key in a contact's `attributes`, and in a campaign's `{{contact.<key>}}`. |
| `name` | string, required | The label the dashboard shows. |
| `type` | string, required<br>`text`, `number`, `date`, `boolean`, `select`, `url` | What the dashboard lets a person enter. `/v1` stores every value as text. |
| `options` | array of string or null, required | A `select` field's choices; null for other types. |
| `position` | integer, required | Its place in the dashboard's order. |
| `agent_access` | string, required<br>`hidden`, `read`, `read_write` | Whether the business's AI agent sees this field's value when it talks to an identified contact. Set in the dashboard. `read_write`: the agent can also update the value after the contact confirms it in a later message (WhatsApp and SMS; `select`, `number` and `boolean` fields). |
| `agent_role` | boolean, required | This `select` field is the contact's role, which the agent can follow role-specific instructions for. At most one per workspace. |

</div>

```json
{
  "id": "fd-0000000001",
  "object": "contact_field",
  "key": "tier",
  "name": "Tier",
  "type": "text",
  "options": [],
  "position": 1,
  "agent_access": "hidden",
  "agent_role": false
}
```