# Platform API reference

> The Platform API (/v1) field by field, generated from the OpenAPI file: one page per resource, with every field, type, value and limit.

<!-- 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 Platform API (`/v1`) field by field: every endpoint, every field, its type, its allowed values and its limits. Generated from [`/openapi/v1.json`](/openapi/v1.json); interactive version: [API reference](/openapi/v1/).

The rules every endpoint shares (base URL, keys, strict bodies, paging, ids) are in the [Overview](/platform-api/overview/#one-shape-for-every-channel); what every error code means is in [Errors](/platform-api/errors/). To get a task done, start from a guide: [Send a text](/platform-api/sms/), [Send a WhatsApp template](/platform-api/whatsapp/), [Place a call](/platform-api/voice/), [Run a campaign](/platform-api/campaigns/), [Receive webhooks](/platform-api/events/) or [Test without spending](/platform-api/testing/).

## Messages

One SMS, MMS or WhatsApp template per request, read back by id, and pictures people text you: [`GET /v1/messages`](/platform-api/reference/messages/#get-v1messages), [`POST /v1/messages`](/platform-api/reference/messages/#post-v1messages), [`GET /v1/messages/{id}`](/platform-api/reference/messages/#get-v1messagesid), [`POST /v1/media`](/platform-api/reference/messages/#post-v1media), [`GET /v1/media/{id}`](/platform-api/reference/messages/#get-v1mediaid). Fields: [Messages](/platform-api/reference/messages/).

## Calls

An AI agent calls one number; read the call and its recording: [`GET /v1/calls`](/platform-api/reference/calls/#get-v1calls), [`POST /v1/calls`](/platform-api/reference/calls/#post-v1calls), [`GET /v1/calls/{id}`](/platform-api/reference/calls/#get-v1callsid), [`GET /v1/calls/{id}/transcript`](/platform-api/reference/calls/#get-v1callsidtranscript), [`POST /v1/calls/{id}/end`](/platform-api/reference/calls/#post-v1callsidend), [`GET /v1/calls/{id}/voicemail`](/platform-api/reference/calls/#get-v1callsidvoicemail), [`GET /v1/recordings/{conversation_id}`](/platform-api/reference/calls/#get-v1recordingsconversation_id), [`GET /v1/voicemail-recordings`](/platform-api/reference/calls/#get-v1voicemail-recordings), [`POST /v1/voicemail-recordings`](/platform-api/reference/calls/#post-v1voicemail-recordings), [`GET /v1/voicemail-recordings/{id}`](/platform-api/reference/calls/#get-v1voicemail-recordingsid), [`DELETE /v1/voicemail-recordings/{id}`](/platform-api/reference/calls/#delete-v1voicemail-recordingsid). Fields: [Calls](/platform-api/reference/calls/).

## Campaigns

One message or call to many people, drafted, previewed and started: [`GET /v1/campaigns`](/platform-api/reference/campaigns/#get-v1campaigns), [`POST /v1/campaigns`](/platform-api/reference/campaigns/#post-v1campaigns), [`GET /v1/campaigns/{id}`](/platform-api/reference/campaigns/#get-v1campaignsid), [`PATCH /v1/campaigns/{id}`](/platform-api/reference/campaigns/#patch-v1campaignsid), [`DELETE /v1/campaigns/{id}`](/platform-api/reference/campaigns/#delete-v1campaignsid), [`POST /v1/campaigns/{id}/start`](/platform-api/reference/campaigns/#post-v1campaignsidstart), [`POST /v1/campaigns/{id}/pause`](/platform-api/reference/campaigns/#post-v1campaignsidpause), [`POST /v1/campaigns/{id}/resume`](/platform-api/reference/campaigns/#post-v1campaignsidresume), [`POST /v1/campaigns/{id}/cancel`](/platform-api/reference/campaigns/#post-v1campaignsidcancel). Fields: [Campaigns](/platform-api/reference/campaigns/).

## Previews, recipients and usage

Who a draft reaches and what it costs, then what happened to each person and what it cost: [`POST /v1/campaigns/{id}/previews`](/platform-api/reference/campaigns/#post-v1campaignsidpreviews), [`GET /v1/campaigns/{id}/previews/{preview_id}`](/platform-api/reference/campaigns/#get-v1campaignsidpreviewspreview_id), [`GET /v1/campaigns/{id}/recipients`](/platform-api/reference/campaigns/#get-v1campaignsidrecipients), [`GET /v1/campaigns/{id}/usage`](/platform-api/reference/campaigns/#get-v1campaignsidusage), [`GET /v1/campaigns/{id}/recipients/{recipient_id}`](/platform-api/reference/campaigns/#get-v1campaignsidrecipientsrecipient_id). Fields: [Campaigns → CampaignPreview](/platform-api/reference/campaigns/#campaignpreview), [CampaignRecipient](/platform-api/reference/campaigns/#campaignrecipient) and [CampaignUsage](/platform-api/reference/campaigns/#campaignusage).

## Rounds

How an AI-call campaign went, in counts, and its next round: [`GET /v1/campaigns/{id}/results`](/platform-api/reference/campaigns/#get-v1campaignsidresults), [`POST /v1/campaigns/{id}/clone`](/platform-api/reference/campaigns/#post-v1campaignsidclone). Fields: [Campaigns → CampaignResults](/platform-api/reference/campaigns/#campaignresults) and [CampaignClone](/platform-api/reference/campaigns/#campaignclone).

## Contacts and contact lists

The people you text and call, and the lists a campaign sends to: [`GET /v1/contacts`](/platform-api/reference/contacts/#get-v1contacts), [`POST /v1/contacts`](/platform-api/reference/contacts/#post-v1contacts), [`GET /v1/contacts/{id}`](/platform-api/reference/contacts/#get-v1contactsid), [`PATCH /v1/contacts/{id}`](/platform-api/reference/contacts/#patch-v1contactsid), [`DELETE /v1/contacts/{id}`](/platform-api/reference/contacts/#delete-v1contactsid), [`POST /v1/contacts/{id}/tags`](/platform-api/reference/contacts/#post-v1contactsidtags), [`DELETE /v1/contacts/{id}/tags/{tag}`](/platform-api/reference/contacts/#delete-v1contactsidtagstag), [`POST /v1/contacts/{id}/dnc`](/platform-api/reference/contacts/#post-v1contactsiddnc), [`GET /v1/contacts/{id}/consent-events`](/platform-api/reference/contacts/#get-v1contactsidconsent-events), [`POST /v1/contacts/{id}/consent-events`](/platform-api/reference/contacts/#post-v1contactsidconsent-events), [`GET /v1/contacts/{id}/channels`](/platform-api/reference/contacts/#get-v1contactsidchannels), [`POST /v1/contacts/{id}/channels`](/platform-api/reference/contacts/#post-v1contactsidchannels), [`DELETE /v1/contacts/{id}/channels/{address}`](/platform-api/reference/contacts/#delete-v1contactsidchannelsaddress), [`GET /v1/contact-fields`](/platform-api/reference/contacts/#get-v1contact-fields), [`GET /v1/lists`](/platform-api/reference/lists/#get-v1lists), [`POST /v1/lists`](/platform-api/reference/lists/#post-v1lists), [`GET /v1/lists/{id}`](/platform-api/reference/lists/#get-v1listsid), [`PATCH /v1/lists/{id}`](/platform-api/reference/lists/#patch-v1listsid), [`DELETE /v1/lists/{id}`](/platform-api/reference/lists/#delete-v1listsid), [`GET /v1/lists/{id}/contacts`](/platform-api/reference/lists/#get-v1listsidcontacts), [`POST /v1/lists/{id}/contacts`](/platform-api/reference/lists/#post-v1listsidcontacts), [`DELETE /v1/lists/{id}/contacts/{contact}`](/platform-api/reference/lists/#delete-v1listsidcontactscontact). Fields: [Contacts](/platform-api/reference/contacts/), [Contact lists](/platform-api/reference/lists/).

## Insights

What each call finds out, as typed fields; its answers are a call's `extracted`: [`GET /v1/insights`](/platform-api/reference/insights/#get-v1insights), [`POST /v1/insights`](/platform-api/reference/insights/#post-v1insights), [`GET /v1/insights/{id}`](/platform-api/reference/insights/#get-v1insightsid), [`PATCH /v1/insights/{id}`](/platform-api/reference/insights/#patch-v1insightsid), [`DELETE /v1/insights/{id}`](/platform-api/reference/insights/#delete-v1insightsid). Fields: [Insights](/platform-api/reference/insights/).

## Senders, templates, agents, audiences, me

The read-only lists you pick from, and the key a request runs as: [`GET /v1/senders`](/platform-api/reference/lookups/#get-v1senders), [`GET /v1/templates`](/platform-api/reference/lookups/#get-v1templates), [`GET /v1/templates/{id}`](/platform-api/reference/lookups/#get-v1templatesid), [`GET /v1/sms-templates`](/platform-api/reference/lookups/#get-v1sms-templates), [`GET /v1/sms-templates/{id}`](/platform-api/reference/lookups/#get-v1sms-templatesid), [`GET /v1/agents`](/platform-api/reference/lookups/#get-v1agents), [`GET /v1/agents/{id}`](/platform-api/reference/lookups/#get-v1agentsid), [`PATCH /v1/agents/{id}`](/platform-api/reference/lookups/#patch-v1agentsid), [`GET /v1/agents/{id}/prompt-versions`](/platform-api/reference/lookups/#get-v1agentsidprompt-versions), [`GET /v1/agents/{id}/prompt-versions/{version}`](/platform-api/reference/lookups/#get-v1agentsidprompt-versionsversion), [`POST /v1/agents/{id}/test-turns`](/platform-api/reference/lookups/#post-v1agentsidtest-turns), [`GET /v1/agents/{id}/phrase-rules`](/platform-api/reference/lookups/#get-v1agentsidphrase-rules), [`POST /v1/agents/{id}/phrase-rules`](/platform-api/reference/lookups/#post-v1agentsidphrase-rules), [`PATCH /v1/agents/{id}/phrase-rules/{rule_id}`](/platform-api/reference/lookups/#patch-v1agentsidphrase-rulesrule_id), [`DELETE /v1/agents/{id}/phrase-rules/{rule_id}`](/platform-api/reference/lookups/#delete-v1agentsidphrase-rulesrule_id), [`GET /v1/segments`](/platform-api/reference/lookups/#get-v1segments), [`POST /v1/segments`](/platform-api/reference/lookups/#post-v1segments), [`GET /v1/segments/{id}`](/platform-api/reference/lookups/#get-v1segmentsid), [`PATCH /v1/segments/{id}`](/platform-api/reference/lookups/#patch-v1segmentsid), [`DELETE /v1/segments/{id}`](/platform-api/reference/lookups/#delete-v1segmentsid), [`GET /v1/segments/{id}/contacts`](/platform-api/reference/lookups/#get-v1segmentsidcontacts), [`GET /v1/me`](/platform-api/reference/lookups/#get-v1me). Fields: [Senders and lookups](/platform-api/reference/lookups/).

## Balance and compliance

The credit balance a send or a campaign start can be refused over, what the credits went on (usage), and the outbound compliance acknowledgment a campaign start can be refused over: [`GET /v1/balance`](/platform-api/reference/account/#get-v1balance), [`GET /v1/usage`](/platform-api/reference/account/#get-v1usage), [`GET /v1/usage/events`](/platform-api/reference/account/#get-v1usageevents), [`GET /v1/usage/export`](/platform-api/reference/account/#get-v1usageexport), [`GET /v1/compliance`](/platform-api/reference/account/#get-v1compliance), [`POST /v1/compliance/accept`](/platform-api/reference/account/#post-v1complianceaccept). Fields: [Balance and compliance](/platform-api/reference/account/).

## Webhooks

Where events are delivered, and how they are signed: [`GET /v1/webhooks`](/platform-api/reference/webhooks/#get-v1webhooks), [`POST /v1/webhooks`](/platform-api/reference/webhooks/#post-v1webhooks), [`GET /v1/webhooks/{id}`](/platform-api/reference/webhooks/#get-v1webhooksid), [`PATCH /v1/webhooks/{id}`](/platform-api/reference/webhooks/#patch-v1webhooksid), [`DELETE /v1/webhooks/{id}`](/platform-api/reference/webhooks/#delete-v1webhooksid), [`POST /v1/webhooks/{id}/rotate-secret`](/platform-api/reference/webhooks/#post-v1webhooksidrotate-secret), [`POST /v1/webhooks/{id}/test`](/platform-api/reference/webhooks/#post-v1webhooksidtest), [`GET /v1/webhooks/{id}/deliveries`](/platform-api/reference/webhooks/#get-v1webhooksiddeliveries), [`GET /v1/webhooks/{id}/deliveries/{delivery_id}`](/platform-api/reference/webhooks/#get-v1webhooksiddeliveriesdelivery_id). Fields: [Webhooks](/platform-api/reference/webhooks/).

## Events

What arrives at an endpoint or on the stream: [`GET /v1/events`](/platform-api/reference/events/#get-v1events). Fields: [Events](/platform-api/reference/events/).

## Errors

Every `/v1` refusal is this envelope, with an HTTP status. What each `code` means and how to fix it: [Errors](/platform-api/errors/).

### V1Error

<div class="ref-table">

| field | type | description |
|---|---|---|
| `error` | object, required |   |
| `error.type` | string, required | Follows the HTTP status: `invalid_request_error` (400, 405, 413, 415, 422), `billing_error` (402), `permission_error` (403), `not_found_error` (404), `conflict_error` (409), `rate_limit_exceeded` (429) or `server_error` (5xx). A missing or bad key is `401` with `invalid_request_error` and code `invalid_api_key`, as OpenAI answers it. Branch on `code`. |
| `error.code` | string, required | Stable machine code. Branch on this. |
| `error.message` | string, required | What went wrong, in a sentence. Show this. |
| `error.param` | string or null, required | What the error is about, in `/v1` names: a body field (`to`, `audience.contacts.0.phone`), a query parameter (`status`), a path segment (`id`) or a header (`idempotency-key`); `null` when it is about none of them. |
| `error.hint` | string | What to do next. |
| `error.docs_url` | string |   |
| `error.request_id` | string | Quote this when you contact support. |
| `error.fields` | array of string | Every field the refusal is about, when there is more than one (`unknown_variable`, `unmapped_variable`): the first is `param`. |
| `error.field` | string | `unmapped_variable`: the placeholder nothing fills. |
| `error.missing` | array of string | `launch_fields_missing`: the `/v1` fields to set before a start. |
| `error.status` | string or null | A state refusal (`campaign_not_pausable`, `campaign_not_cancellable`, `campaign_not_resumable`, `campaign_not_previewable`, `campaign_not_editable`, `campaign_not_deletable`): the campaign's status now. |
| `error.allowed` | array of string | A state refusal (`campaign_not_pausable`, `campaign_not_cancellable`, `campaign_not_resumable`, `campaign_not_previewable`): the statuses the action takes. A cancel never lists `draft`: a draft is deleted instead. |
| `error.problems` | array of objects | `invalid_variable`, `unknown_variable`, `unmapped_variable`: every variable the refusal is about, one entry each with its own `message` and `hint`, ordered by `param`; the first is the refusal's own `param`, `message` and `hint`. `field` is the variable's name (or the placeholder nothing fills). |
| `error.problems[].field` | string, required |   |
| `error.problems[].param` | string, required |   |
| `error.problems[].message` | string, required |   |
| `error.problems[].hint` | string |   |
| `error.problems[].suggestion` | string | `unknown_variable`: the closest name the content has. |
| `error.part` | string<br>`text`, `field`, `fallback` | `invalid_variable` for a value over 200 characters: which part of the first one is too long (the plain text, the field name, or the fallback). |
| `error.template_status` | string | `template_not_approved`: the template's status. |
| `error.retry_after` | number | Seconds to wait before retrying, as in `Retry-After`. |
| `error.short_credits` | number | `insufficient_credits`: how many more credits the send needs. |
| `error.campaign_id` | string | A refusal after the campaign was created: the campaign, kept as a draft. |
| `error.issues` | array of objects | `invalid_request`: every problem the body has, one per entry. |
| `error.issues[].path` | array of string, required |   |
| `error.issues[].code` | string, required |   |
| `error.issues[].message` | string, required |   |
| `error.channel` | string | `field_not_for_channel`: the campaign's channel (`fields` names the fields of another channel). |
| `error.errors` | array of any | An audience refusal (`empty_audience_input`, an import with no usable row): the rows that could not be read, one entry each (`[]` when there were none). |
| `error.received` | number | An audience refusal: how many rows were sent. |
| `error.invalid_rows` | number | An audience refusal: how many rows could not be used. |
| `error.sender_status` | string | `sender_not_cleared`: the number's `sms.status` in `GET /v1/senders` (`receiving_only`, `in_review`, `rejected`, …). |
| `error.skipped_by_reason` | map of string to number | `no_recipients`: how many of the audience are left out, by reason (`no_phone_number`, `opted_out`, `duplicate`, …), accounting for every member. |
| `error.reason` | string or null | Why, in a word, where a code has several causes: `billing_not_eligible` (no card on file, or a past-due or canceled subscription). |
| `error.message_id` | string or null | A WhatsApp send refused after the message was stored (`opted_out`, `send_failed`): the stored message's id, for `GET /v1/messages/{id}`. |
| `error.version` | string or null | `outbound_compliance_required`: the acknowledgment version to accept (`POST /v1/compliance/accept`). |
| `error.current_version` | string | `version_mismatch` (`POST /v1/compliance/accept`): the acknowledgment's version now; read it with `GET /v1/compliance` and accept that one. |
| `error.candidates` | array of string | `ambiguous_contact`: the `ct-…` ids of the contacts that hold the number; remove one by its id. |
| `error.campaigns` | array of string | `list_in_use`: the `ca-…` ids of the campaigns not yet finished that use the list. |
| `error.contact` | string | `contact_exists`: the `ct-…` id of the contact that already has the phone or email. |
| `error.channels` | array of string | `stopped_by_person`, `no_address` (`POST /v1/contacts/{id}/dnc`): the channels the refusal is about. |
| `error.cap` | number | `daily_voice_minutes_exceeded`: the workspace's daily talk-minute cap. `spend_cap_reached`, `spend_cap_raise_needs_person`: the campaign's spending limit, in credits. |
| `error.used` | number | `daily_voice_minutes_exceeded`: what the day already owes: `metered` + `held` minutes. `spend_cap_reached`: the credits the campaign has been billed. |
| `error.remaining` | number | `daily_voice_minutes_exceeded`: the minutes left on `day`. `spend_cap_reached`: the credits left under the limit after the calls under way (`cap − used − in_flight`). |
| `error.in_flight` | number | `spend_cap_reached`: the projected credits of the campaign's calls under way. |
| `error.next_call` | number | `spend_cap_reached`: the projected credits of one more call (a median call on the campaign's agent). |
| `error.dashboard_url` | string | `spend_cap_reached`, `spend_cap_raise_needs_person`: the campaign's page in the dashboard, where a person raises its spending limit. |
| `error.metered` | number | `daily_voice_minutes_exceeded`: voice minutes already billed on `day`. |
| `error.held` | number | `daily_voice_minutes_exceeded`: minutes held on `day` by queued or scheduled campaigns that start that day or earlier. |
| `error.held_by` | array of objects | `daily_voice_minutes_exceeded`: the campaigns holding `held`, largest first. |
| `error.held_by[].campaign` | string, required | The campaign's `ca-…` id. |
| `error.held_by[].name` | string, required |   |
| `error.held_by[].minutes` | number, required |   |
| `error.held_by[].starts_at` | string or null, required |   |
| `error.day` | string or null | `daily_voice_minutes_exceeded`: the budget day checked (`YYYY-MM-DD` in `time_zone`): the campaign's start day. |
| `error.time_zone` | string or null | `daily_voice_minutes_exceeded`: the zone the workspace's budget day follows (UTC when none is set). |
| `error.per_call_minutes` | number or null | `daily_voice_minutes_exceeded`: the minutes each call is projected at (this workspace's average, at least 1.5). |
| `error.resets_at` | string or null | When the daily allowance clears (ISO 8601): `trial_daily_cap` for the trial number's calls, `daily_voice_minutes_exceeded` for the voice budget (midnight in `time_zone`). |
| `error.allowed_country` | string or null | `route_blocked`: the country your calling rules let you call. |
| `error.closes_at` | string or null | `reply_window_closed`: when Meta's 24-hour window closed (ISO 8601), or null when the person never wrote. |

</div>