API reference
Every operation on the API, generated from the same OpenAPI document the API is checked against, so it cannot fall behind. What holds for all of them (credentials, pagination, idempotency, rate limits) is on REST API conventions.
Operations are grouped by the path they share. Scopes are the ones a key or token must carry; some operations need a credential but no scope, and a few declare no credential in the contract. Any operation can answer with the standard error envelope described in the error reference; the Errors line under an operation lists the codes its own text names, which is not always every code it can raise.
/v1/account
GET /v1/account/data-policy
What is kept, in which group, for how long, and any queued erasure
Scopes settings:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "data_policy" | required | |
| groups | object[] | required | |
| groups[].key | "conversation_data" | "operational_records" | "account_activity" | required | |
| groups[].label | string | required | |
| groups[].contains | string[] | required | |
| groups[].adjustable | boolean | required | |
| groups[].retention_months | integer | null | required | |
| groups[].floor_months | integer | null | required | |
| groups[].ceiling_months | integer | null | required | |
| survives_account_deletion | object[] | required | |
| survives_account_deletion[].what | "billing_and_invoices" | "proof_of_erasure" | required | |
| survives_account_deletion[].years | integer | required | |
| survives_account_deletion[].basis | string | required | |
| queued_erasure | object | null | required | |
| queued_erasure.id | string | required | |
| queued_erasure.requested_by | string | required | |
| queued_erasure.requested_by_name | string | null | required | |
| queued_erasure.requested_at | string (date-time) | required | |
| queued_erasure.executes_at | string (date-time) | required | |
| queued_erasure.seconds_remaining | integer | required | |
| queued_erasure.can_i_cancel | boolean | required |
PATCH /v1/account/data-policy
Set the two adjustable retention groups
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| conversation_data_months | integer | optional | |
| operational_records_months | integer | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "data_policy" | required | |
| groups | object[] | required | |
| groups[].key | "conversation_data" | "operational_records" | "account_activity" | required | |
| groups[].label | string | required | |
| groups[].contains | string[] | required | |
| groups[].adjustable | boolean | required | |
| groups[].retention_months | integer | null | required | |
| groups[].floor_months | integer | null | required | |
| groups[].ceiling_months | integer | null | required | |
| survives_account_deletion | object[] | required | |
| survives_account_deletion[].what | "billing_and_invoices" | "proof_of_erasure" | required | |
| survives_account_deletion[].years | integer | required | |
| survives_account_deletion[].basis | string | required | |
| queued_erasure | object | null | required | |
| queued_erasure.id | string | required | |
| queued_erasure.requested_by | string | required | |
| queued_erasure.requested_by_name | string | null | required | |
| queued_erasure.requested_at | string (date-time) | required | |
| queued_erasure.executes_at | string (date-time) | required | |
| queued_erasure.seconds_remaining | integer | required | |
| queued_erasure.can_i_cancel | boolean | required |
POST /v1/account/erasure
Queue the whole-workspace erasure. 7-day grace period
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| confirm | string | required |
Response 202
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "account_erasure" | required | |
| erasure | object | required | |
| erasure.id | string | required | |
| erasure.requested_by | string | required | |
| erasure.requested_by_name | string | null | required | |
| erasure.requested_at | string (date-time) | required | |
| erasure.executes_at | string (date-time) | required | |
| erasure.seconds_remaining | integer | required | |
| erasure.can_i_cancel | boolean | required |
DELETE /v1/account/erasure
Cancel a queued workspace erasure
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "account_erasure" | required | |
| state | "cancelled" | required | |
| cancelled_at | string (date-time) | required |
POST /v1/account/erasure/cancel/{token}
Cancel a queued workspace erasure from the emailed link. Single-use
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "account_erasure" | required | |
| state | "cancelled" | required | |
| cancelled_at | string (date-time) | required |
/v1/ai
GET /v1/ai/authoring
One WhatsApp number’s AI authoring conversations, the most recently active first, each with its title and the automations it holds
Scopes automations:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | required | The WhatsApp number (connection id) whose AI authoring conversations to list. |
| limit | query | integer | optional | |
| after | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "authoring_conversation" | required | |
| data[].title | string | null | required | The owner's first message, cut to 80 characters. Null until the owner has written one. |
| data[].automation_ids | string[] | required | The automations this conversation holds, the one it took on most recently first: each it built or changed, and the one it was started on. No other conversation holds them. |
| data[].created_at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required | When the conversation last changed. The list is newest first by this. |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/ai/authoring
Start an AI authoring conversation on one WhatsApp number. It builds and changes that number’s automations only, and never switches one on
Scopes automations:writeautomations:readruns:writeruns:readconnectors:readconnectors:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | The WhatsApp number (connection id) the conversation builds and changes automations for. It touches no other number’s. |
| automation_id | string | optional | An automation on that number for the conversation to start on, to fix or improve it. Refused when another conversation already holds it (that conversation’s id is named; open it instead), and for an automation made from a recipe, the AI receptionist included. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "authoring_conversation" | required | |
| connection_id | string | required | |
| automation_ids | string[] | required | The automations this conversation holds, the one it took on most recently first: each it built or changed, and the one it was started on. No other conversation holds them. |
| automations | object[] | required | Each of automation_ids, in the same order, with its name and state. |
| automations[].id | string | required | |
| automations[].name | string | required | |
| automations[].enabled | boolean | required | Whether the owner has switched it on. |
| automations[].has_active_version | boolean | required | Whether a version of it was ever switched on: false until the owner does. |
| messages | object[] | required | |
| messages[].role | "owner" | required | |
| messages[].text | string | required | |
| messages[].created_at | string (date-time) | required | |
| messages[].reply | object | required | |
| messages[].reply.object | "authoring_reply" | required | |
| messages[].reply.conversation_id | string | required | |
| messages[].reply.outcome | "built" | "changed" | "cannot" | "question" | "stopped" | required | built or changed: an automation was made or changed and tested, switched off. cannot: no block does what was asked, and nothing was made. question: it needs an answer from the owner. stopped: it stopped before finishing, and text says why and what still fails. |
| messages[].reply.text | string | required | What the owner is told, in plain words. |
| messages[].reply.summary | string | null | required | The automation in plain words: "when a customer writes X, it does Y, then Z". Null when there is none. |
| messages[].reply.automation_id | string | null | required | The automation this answer built or changed, or null when it built and changed none. Always switched off: the owner switches it on. |
| messages[].reply.test | object | null | required | The last Test this answer read, or null when it read none. |
| messages[].reply.connect_needed | object[] | required | Integrations a step needs, for the owner to connect each one that is not connected for this number. |
| messages[].reply.created_at | string (date-time) | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required |
GET /v1/ai/authoring/{id}
One AI authoring conversation: what the owner said, and each answer
Scopes automations:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "authoring_conversation" | required | |
| connection_id | string | required | |
| automation_ids | string[] | required | The automations this conversation holds, the one it took on most recently first: each it built or changed, and the one it was started on. No other conversation holds them. |
| automations | object[] | required | Each of automation_ids, in the same order, with its name and state. |
| automations[].id | string | required | |
| automations[].name | string | required | |
| automations[].enabled | boolean | required | Whether the owner has switched it on. |
| automations[].has_active_version | boolean | required | Whether a version of it was ever switched on: false until the owner does. |
| messages | object[] | required | |
| messages[].role | "owner" | required | |
| messages[].text | string | required | |
| messages[].created_at | string (date-time) | required | |
| messages[].reply | object | required | |
| messages[].reply.object | "authoring_reply" | required | |
| messages[].reply.conversation_id | string | required | |
| messages[].reply.outcome | "built" | "changed" | "cannot" | "question" | "stopped" | required | built or changed: an automation was made or changed and tested, switched off. cannot: no block does what was asked, and nothing was made. question: it needs an answer from the owner. stopped: it stopped before finishing, and text says why and what still fails. |
| messages[].reply.text | string | required | What the owner is told, in plain words. |
| messages[].reply.summary | string | null | required | The automation in plain words: "when a customer writes X, it does Y, then Z". Null when there is none. |
| messages[].reply.automation_id | string | null | required | The automation this answer built or changed, or null when it built and changed none. Always switched off: the owner switches it on. |
| messages[].reply.test | object | null | required | The last Test this answer read, or null when it read none. |
| messages[].reply.connect_needed | object[] | required | Integrations a step needs, for the owner to connect each one that is not connected for this number. |
| messages[].reply.created_at | string (date-time) | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required |
POST /v1/ai/authoring/{id}/messages
Send the owner’s message. The answer: what it built (switched off), in plain words, its Test result, and what to connect
Scopes automations:writeautomations:readruns:writeruns:readconnectors:readconnectors:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| message | string | required | What the owner needs, or their answer to a question, in their own words. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "authoring_reply" | required | |
| conversation_id | string | required | |
| outcome | "built" | "changed" | "cannot" | "question" | "stopped" | required | built or changed: an automation was made or changed and tested, switched off. cannot: no block does what was asked, and nothing was made. question: it needs an answer from the owner. stopped: it stopped before finishing, and text says why and what still fails. |
| text | string | required | What the owner is told, in plain words. |
| summary | string | null | required | The automation in plain words: "when a customer writes X, it does Y, then Z". Null when there is none. |
| automation_id | string | null | required | The automation this answer built or changed, or null when it built and changed none. Always switched off: the owner switches it on. |
| test | object | null | required | The last Test this answer read, or null when it read none. |
| test.run_id | string | required | |
| test.status | "queued" | "running" | "waiting" | "success" | "failure" | "cancelled" | "timeout" | "skipped" | … | required | |
| test.passed | boolean | required | |
| test.reason | string | null | required | Why it did not pass, in plain words. Null when it passed. |
| test.step_id | string | null | required | The step it stopped at, when one did. |
| connect_needed | object[] | required | Integrations a step needs, for the owner to connect each one that is not connected for this number. |
| connect_needed[].connector | string | required | |
| connect_needed[].why | string | required | |
| connect_needed[].connected | boolean | required | Whether the integration was connected for this number when this answer was made. Read the connector accounts for whether it is now. |
| created_at | string (date-time) | required |
POST /v1/ai/try
Try an AI assistant (automation_id): its reply from its own persona and knowledge, and what it would do. Nothing is sent; each chat counts as one AI conversation
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| message | string | required | |
| history | object[] | optional | |
| history[].role | "customer" | "assistant" | required | |
| history[].text | string | required | |
| history[].handed_over | boolean | optional | True on the customer turn after which a person had the chat: its answer was cant_answer, and the automation's steps for that case hand the chat to a person. From then on, as in a real chat, the answer is outcome person_has_chat and the model is not asked. |
| automation_id | string | required | The AI assistant being tried: an automation of this workspace with one AI step. The answer comes from its own persona and knowledge, never another assistant’s. Alone, the try also uses the escalate_keywords of the version its conversations run (the switched-on one, else the newest), the first message of its branch for the cant_answer outcome of that step and its cause, and its closed message and opening hours; with connection_id, the set-up’s answers replace those. |
| connection_id | string | optional | For a receptionist not switched on yet: the number it is being set up on. Requires holding_message; escalate_keywords and closed as the set-up has them. They replace the settings of automation_id’s version. |
| holding_message | string | optional | Required with connection_id, and only with it: the message a customer is sent when the assistant cannot answer. |
| escalate_keywords | string[] | optional | With connection_id: the words that pass a chat to a person. |
| closed | object | optional | With connection_id: the set-up's closed message, opening hours (HH:MM, in the workspace's timezone) and closed-hours passing message. While closed, the answer's closed_message is the message with the hours, and a customer_message for a chat passed on is the closed-hours one. |
| closed.message | string | required | |
| closed.open_days | ("mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun")[] | required | |
| closed.opens_at | string | required | |
| closed.closes_at | string | required | |
| closed.holding_message | string | optional | The set-up's message for when the assistant passes the chat on while closed, in place of holding_message: where it says {opens}, the customer reads when the business next opens. The AI receptionist's default when left out. |
| chat_id | string | null | optional | The Try it chat this message continues: the chat_id an earlier answer in it gave. Each chat counts as one AI conversation, billed like a real one, from the first time the assistant is asked in it. Null or left out starts a new chat; a history that already holds an assistant turn needs it, unless a turn in it was handed over. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| reply | string | null | required | |
| decision | object | required | |
| decision.intent | string | null | required | |
| decision.confidence | number | null | required | |
| decision.needs_human | boolean | required | |
| decision.filtered_reason | string | null | required | |
| decision.would_hand_over | boolean | required | True when the assistant would not answer: the automation's own steps for that case would. |
| sources | object[] | required | |
| sources[].source_id | string | required | |
| sources[].title | string | required | |
| sources[].snippet | string | required | |
| outcome | "answered" | "cant_answer" | "person_has_chat" | required | answered: the customer gets the reply. cant_answer: the assistant cannot answer, cant_answer says why, and the automation's own steps for that case run. person_has_chat: an earlier turn was handed to a person, so the assistant would not see this message. |
| cant_answer | object | null | required | Null unless outcome is cant_answer. |
| cant_answer.cause | "harmful" | "prices_off" | "not_known" | "keyword" | "model_down" | required | harmful: a harmful request (the assistant read it so, or the model refused it). prices_off: a price question while the assistant may not give prices. not_known: a question it cannot answer from what it knows, a reply held back, or a message it cannot read. keyword: the customer wrote one of the owner's words, or asked for a person. model_down: no answer could be had from the model, or the AI budget or allowance is spent. |
| cant_answer.reason | string | required | Why, as the owner reads it. |
| cant_answer.filtered_reason | string | null | required | Why the reply was held back, when it was. |
| cant_answer.setting | "may_quote_prices" | "links" | "never_say" | "escalate_keywords" | null | required | The owner's own setting it came from: one of the persona's (may_quote_prices, links, never_say), or the receptionist's escalate_keywords. |
| customer_message | string | null | required | What the customer would receive: the reply, or, when the assistant cannot answer, the message the automation sends then, word for word. While closed, that is its closed-hours message when that is what it sends, with the next opening filled in. Null when nothing would be sent (person_has_chat, or an automation whose branch for that cause sends no message), or when the assistant cannot answer and the request names no automation_id and no connection_id. |
| closed_message | string | null | required | The owner's closed message with the opening hours, sent before the first reply while the automation's hours say closed, whatever that reply is: once per conversation. Null otherwise, and null when customer_message already says the business is closed (the customer gets one such message, not two). |
| chat_id | string | null | required | This Try it chat, one AI conversation: send it back as chat_id with the next message. Null while the assistant has not been asked in the chat (the owner’s keyword asks it nothing). |
/v1/ai-connections
GET /v1/ai-connections
The AI apps connected to the workspace; an owner or admin sees all, a member their own. Dashboard sessions only
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "ai_connection" | required | |
| data[].client | object | required | |
| data[].client.id | string | required | The app’s client id as it signed in: the address of its details, or the id Cosend gave it when it registered. |
| data[].client.name | string | required | What the app calls itself. Not verified. |
| data[].connected_by | object | required | |
| data[].connected_by.id | string | required | The person who approved the app (a usr_ id). |
| data[].connected_by.name | string | required | |
| data[].scopes | object[] | required | What the app may do. |
| data[].scopes[].scope | string | required | |
| data[].scopes[].description | string | required | What the scope lets the app do, in plain words. |
| data[].created_at | string (date-time) | required | |
| data[].last_used_at | string (date-time) | null | required | When the app last called Cosend, to the minute. Null until its first call. |
| data[].removed_at | string (date-time) | null | required | When the app was removed and lost its access. Null while it is connected. |
DELETE /v1/ai-connections/{id}
Remove an AI app: its access ends at once. Removing it again answers the same. Dashboard sessions only
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "ai_connection" | required | |
| client | object | required | |
| client.id | string | required | The app’s client id as it signed in: the address of its details, or the id Cosend gave it when it registered. |
| client.name | string | required | What the app calls itself. Not verified. |
| connected_by | object | required | |
| connected_by.id | string | required | The person who approved the app (a usr_ id). |
| connected_by.name | string | required | |
| scopes | object[] | required | What the app may do. |
| scopes[].scope | string | required | |
| scopes[].description | string | required | What the scope lets the app do, in plain words. |
| created_at | string (date-time) | required | |
| last_used_at | string (date-time) | null | required | When the app last called Cosend, to the minute. Null until its first call. |
| removed_at | string (date-time) | null | required | When the app was removed and lost its access. Null while it is connected. |
/v1/api-keys
GET /v1/api-keys
The organization’s keys. The key itself is never in a list response
Scopes keys:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "api_key" | required | |
| data[].name | string | required | |
| data[].kind | "connection" | "organization" | required | |
| data[].connection_id | string | null | required | The first of connection_ids, or null for a workspace key. Read connection_ids: a key may work on several numbers. |
| data[].connection_ids | string[] | required | The WhatsApp numbers (connection ids) this key works on: one or more for a connection key, none for a workspace key, which works on every number. The key is refused on anything of another number. |
| data[].scopes | string[] | required | |
| data[].display_prefix | string | required | |
| data[].last_used_at | string (date-time) | null | required | |
| data[].created_at | string (date-time) | required | |
| data[].expires_at | string (date-time) | null | required | |
| data[].revoked_at | string (date-time) | null | required |
POST /v1/api-keys
Create a key. THE ONLY RESPONSE THAT EVER CARRIES key
Scopes keys:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | |
| kind | "connection" | "organization" | required | |
| connection_ids | string[] | optional | For a connection key: the WhatsApp numbers (connection ids) it works on, one or more. Leave it out for a workspace key, which works on every number. |
| connection_id | string | null | optional | One number, read together with connection_ids. Prefer connection_ids. |
| scopes | ("messages:read" | "messages:write" | "contacts:read" | "contacts:write" | "templates:read" | "templates:write" | "notifications:read" | "notifications:write" | …)[] | optional | |
| is_live | boolean | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "api_key" | required | |
| name | string | required | |
| kind | "connection" | "organization" | required | |
| connection_id | string | null | required | The first of connection_ids, or null for a workspace key. Read connection_ids: a key may work on several numbers. |
| connection_ids | string[] | required | The WhatsApp numbers (connection ids) this key works on: one or more for a connection key, none for a workspace key, which works on every number. The key is refused on anything of another number. |
| scopes | string[] | required | |
| display_prefix | string | required | |
| last_used_at | string (date-time) | null | required | |
| created_at | string (date-time) | required | |
| expires_at | string (date-time) | null | required | |
| revoked_at | string (date-time) | null | required | |
| key | string | required |
DELETE /v1/api-keys/{id}
Immediate revocation
Scopes keys:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "api_key" | required | |
| name | string | required | |
| kind | "connection" | "organization" | required | |
| connection_id | string | null | required | The first of connection_ids, or null for a workspace key. Read connection_ids: a key may work on several numbers. |
| connection_ids | string[] | required | The WhatsApp numbers (connection ids) this key works on: one or more for a connection key, none for a workspace key, which works on every number. The key is refused on anything of another number. |
| scopes | string[] | required | |
| display_prefix | string | required | |
| last_used_at | string (date-time) | null | required | |
| created_at | string (date-time) | required | |
| expires_at | string (date-time) | null | required | |
| revoked_at | string (date-time) | null | required |
POST /v1/api-keys/{id}/rotate
Issue a replacement; the old key expires after grace_hours
Scopes keys:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| grace_hours | integer | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| created | object | required | |
| created.id | string | required | |
| created.object | "api_key" | required | |
| created.name | string | required | |
| created.kind | "connection" | "organization" | required | |
| created.connection_id | string | null | required | The first of connection_ids, or null for a workspace key. Read connection_ids: a key may work on several numbers. |
| created.connection_ids | string[] | required | The WhatsApp numbers (connection ids) this key works on: one or more for a connection key, none for a workspace key, which works on every number. The key is refused on anything of another number. |
| created.scopes | string[] | required | |
| created.display_prefix | string | required | |
| created.last_used_at | string (date-time) | null | required | |
| created.created_at | string (date-time) | required | |
| created.expires_at | string (date-time) | null | required | |
| created.revoked_at | string (date-time) | null | required | |
| created.key | string | required | |
| rotated | object | required | |
| rotated.id | string | required | |
| rotated.object | "api_key" | required | |
| rotated.name | string | required | |
| rotated.kind | "connection" | "organization" | required | |
| rotated.connection_id | string | null | required | The first of connection_ids, or null for a workspace key. Read connection_ids: a key may work on several numbers. |
| rotated.connection_ids | string[] | required | The WhatsApp numbers (connection ids) this key works on: one or more for a connection key, none for a workspace key, which works on every number. The key is refused on anything of another number. |
| rotated.scopes | string[] | required | |
| rotated.display_prefix | string | required | |
| rotated.last_used_at | string (date-time) | null | required | |
| rotated.created_at | string (date-time) | required | |
| rotated.expires_at | string (date-time) | null | required | |
| rotated.revoked_at | string (date-time) | null | required |
/v1/approval-action-types
GET /v1/approval-action-types
Every action type that can require approval. The settings screen renders this
Scopes approvals:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].action_type | string | required | |
| data[].object | "approval_action_type" | required | |
| data[].display_name | string | required | |
| data[].description | string | required | |
| data[].default_risk | "low" | "medium" | "high" | required |
/v1/approval-settings
GET /v1/approval-settings
Every action type and this org’s mode for it. Admins only
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].action_type | string | required | |
| data[].object | "approval_setting" | required | |
| data[].display_name | string | required | |
| data[].description | string | required | |
| data[].default_risk | "low" | "medium" | "high" | required | |
| data[].mode | "ask" | "auto_approve" | "auto_reject" | required | |
| data[].effective_mode | "ask" | "auto_approve" | "auto_reject" | required | |
| data[].consent | object | null | required | |
| data[].consent.mode | "auto_approve" | "auto_reject" | required | |
| data[].consent.by | string | null | required | |
| data[].consent.at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required |
PATCH /v1/approval-settings
Set one type to ask, auto-approve or auto-reject. Admins only; the risk is recorded
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| action_type | string | required | |
| mode | "ask" | "auto_approve" | "auto_reject" | required | |
| risk_acknowledged | boolean | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| action_type | string | required | |
| object | "approval_setting" | required | |
| display_name | string | required | |
| description | string | required | |
| default_risk | "low" | "medium" | "high" | required | |
| mode | "ask" | "auto_approve" | "auto_reject" | required | |
| effective_mode | "ask" | "auto_approve" | "auto_reject" | required | |
| consent | object | null | required | |
| consent.mode | "auto_approve" | "auto_reject" | required | |
| consent.by | string | null | required | |
| consent.at | string (date-time) | required | |
| updated_at | string (date-time) | required |
/v1/approvals
GET /v1/approvals
Pending human decisions
Scopes approvals:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| state | query | "pending" | "approved" | "rejected" | "expired" | "executed" | "failed" | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "approval" | required | |
| data[].action | string | required | |
| data[].state | "pending" | "approved" | "rejected" | "expired" | "executed" | "failed" | required | |
| data[].risk | "low" | "medium" | "high" | required | |
| data[].preview | string | required | |
| data[].arguments | object | required | |
| data[].requested_by | "user" | "api_key" | "automation" | "system" | required | |
| data[].requester_id | string | null | required | |
| data[].requester | object | null | required | |
| data[].requester.kind | "automation" | "api_key" | "user" | required | |
| data[].requester.id | string | null | required | |
| data[].requester.name | string | null | required | |
| data[].run_id | string | null | required | |
| data[].conversation_id | string | null | required | |
| data[].connection_id | string | null | required | |
| data[].automation_id | string | null | required | |
| data[].decided_via | "human" | "auto_rule" | "expiry" | null | required | |
| data[].decided_by | string | null | required | |
| data[].decided_at | string (date-time) | null | required | |
| data[].decision_note | string | null | required | |
| data[].expires_at | string (date-time) | required | |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/approvals
Hold an action for a human decision. Expires in 24 hours, and an expiry is a rejection
Scopes approvals:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| action | string | required | |
| arguments | object | optional | |
| preview | string | required | |
| risk | "low" | "medium" | "high" | optional | |
| run_id | string | null | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "approval" | required | |
| action | string | required | |
| state | "pending" | "approved" | "rejected" | "expired" | "executed" | "failed" | required | |
| risk | "low" | "medium" | "high" | required | |
| preview | string | required | |
| arguments | object | required | |
| requested_by | "user" | "api_key" | "automation" | "system" | required | |
| requester_id | string | null | required | |
| requester | object | null | required | |
| requester.kind | "automation" | "api_key" | "user" | required | |
| requester.id | string | null | required | |
| requester.name | string | null | required | |
| run_id | string | null | required | |
| conversation_id | string | null | required | |
| connection_id | string | null | required | |
| automation_id | string | null | required | |
| decided_via | "human" | "auto_rule" | "expiry" | null | required | |
| decided_by | string | null | required | |
| decided_at | string (date-time) | null | required | |
| decision_note | string | null | required | |
| expires_at | string (date-time) | required | |
| created_at | string (date-time) | required |
GET /v1/approvals/{id}
One approval, including preview, exactly what the human is shown
Scopes approvals:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "approval" | required | |
| action | string | required | |
| state | "pending" | "approved" | "rejected" | "expired" | "executed" | "failed" | required | |
| risk | "low" | "medium" | "high" | required | |
| preview | string | required | |
| arguments | object | required | |
| requested_by | "user" | "api_key" | "automation" | "system" | required | |
| requester_id | string | null | required | |
| requester | object | null | required | |
| requester.kind | "automation" | "api_key" | "user" | required | |
| requester.id | string | null | required | |
| requester.name | string | null | required | |
| run_id | string | null | required | |
| conversation_id | string | null | required | |
| connection_id | string | null | required | |
| automation_id | string | null | required | |
| decided_via | "human" | "auto_rule" | "expiry" | null | required | |
| decided_by | string | null | required | |
| decided_at | string (date-time) | null | required | |
| decision_note | string | null | required | |
| expires_at | string (date-time) | required | |
| created_at | string (date-time) | required |
POST /v1/approvals/{id}/approve
Approve the held action. An expired approval is rejected instead, never approved
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| note | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "approval" | required | |
| action | string | required | |
| state | "pending" | "approved" | "rejected" | "expired" | "executed" | "failed" | required | |
| risk | "low" | "medium" | "high" | required | |
| preview | string | required | |
| arguments | object | required | |
| requested_by | "user" | "api_key" | "automation" | "system" | required | |
| requester_id | string | null | required | |
| requester | object | null | required | |
| requester.kind | "automation" | "api_key" | "user" | required | |
| requester.id | string | null | required | |
| requester.name | string | null | required | |
| run_id | string | null | required | |
| conversation_id | string | null | required | |
| connection_id | string | null | required | |
| automation_id | string | null | required | |
| decided_via | "human" | "auto_rule" | "expiry" | null | required | |
| decided_by | string | null | required | |
| decided_at | string (date-time) | null | required | |
| decision_note | string | null | required | |
| expires_at | string (date-time) | required | |
| created_at | string (date-time) | required |
POST /v1/approvals/{id}/reject
Reject the held action
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| note | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "approval" | required | |
| action | string | required | |
| state | "pending" | "approved" | "rejected" | "expired" | "executed" | "failed" | required | |
| risk | "low" | "medium" | "high" | required | |
| preview | string | required | |
| arguments | object | required | |
| requested_by | "user" | "api_key" | "automation" | "system" | required | |
| requester_id | string | null | required | |
| requester | object | null | required | |
| requester.kind | "automation" | "api_key" | "user" | required | |
| requester.id | string | null | required | |
| requester.name | string | null | required | |
| run_id | string | null | required | |
| conversation_id | string | null | required | |
| connection_id | string | null | required | |
| automation_id | string | null | required | |
| decided_via | "human" | "auto_rule" | "expiry" | null | required | |
| decided_by | string | null | required | |
| decided_at | string (date-time) | null | required | |
| decision_note | string | null | required | |
| expires_at | string (date-time) | required | |
| created_at | string (date-time) | required |
/v1/auth
GET /v1/auth/invitation
Is there a valid invitation for the signed-in address? Workspace and role, or why not
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "invitation_check" | required | |
| status | "valid" | "expired" | "none" | required | |
| invitations | object[] | required | |
| invitations[].organization_id | string | required | |
| invitations[].organization_name | string | required | |
| invitations[].role | "owner" | "admin" | "member" | required |
POST /v1/auth/join-request
Ask to join the workspace on your email domain. The same 202 whether or not one exists
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| user_name | string | required |
Response 202
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "join_request" | required | |
| domain | string | required |
POST /v1/auth/provision
Provision a user and organization, and mint their first dashboard token
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| user_name | string | required | |
| organization_id | string | optional | |
| organization_name | string | optional | |
| organization_slug | string | optional | |
| timezone | string | optional | |
| from | string | null | optional | |
| utm_source | string | null | optional | |
| utm_medium | string | null | optional | |
| utm_campaign | string | null | optional | |
| utm_term | string | null | optional | |
| utm_content | string | null | optional | |
| landing_path | string | null | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | required | |
| expires_at | string (date-time) | required | |
| organization_id | string | required | |
| role | "owner" | "admin" | "member" | required |
POST /v1/auth/refresh
Re-mint a 15-minute dashboard token for an already-provisioned user
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| organization_id | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | required | |
| expires_at | string (date-time) | required | |
| organization_id | string | required | |
| role | "owner" | "admin" | "member" | required |
POST /v1/auth/repeated-signup
Tell us a signup came back as a duplicate. Always 204, whatever the address
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string (email) | required |
Response 204
No content
POST /v1/auth/workspace-address
Is this workspace address free? One 200 shape for every verdict, shape problems named
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| slug | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "workspace_address" | required | |
| slug | string | required | |
| available | boolean | required | |
| problem | "too_short" | "too_long" | "bad_characters" | "hyphen_at_edge" | "taken" | null | required |
/v1/automation-catalogue
GET /v1/automation-catalogue
Every block and trigger an automation is built from, with its inputs and outputs
Scopes automations:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "automation_catalogue" | required | |
| version | string | required | |
| blocks | object[] | required | |
| blocks[].name | string | required | |
| blocks[].node | string | required | |
| blocks[].version | string | required | |
| blocks[].category | string | required | |
| blocks[].display_name | string | required | |
| blocks[].description | string | required | |
| blocks[].input_schema | object | required | The JSON Schema of what the step takes, with the recipe forms’ picker where an input is one (tag, template, language, sheet, account). A send’s to is picker: "recipient", with two choices: customer, “The customer who wrote”, which binds binds ({ "kind": "ref", "path": "contact.id" }) and is offered, and chosen first, on the starts its starts lists; and number, “A fixed number”, typed with its country code as its description says. Validate refuses a send with neither (missing_recipient) and a number the phone rule refuses (unusable_recipient), in plain words naming the step, at <step>/inputs/to. Every other input naming one contact is picker: "contact", with the same two choices, refused as missing_contact and unusable_contact; every input naming a chat is picker: "conversation", with one choice, chat, “This chat”, which binds { "kind": "ref", "path": "conversation.id" } and is offered, and chosen first, on the starts its starts lists, refused left empty as missing_conversation. Each refusal is at <step>/inputs/<input>. |
| blocks[].output_schema | object | required | |
| blocks[].side_effect | "none" | "internal_write" | "external_send" | "money" | required | |
| blocks[].destructive | boolean | required | |
| blocks[].requires_connection | string | null | required | |
| blocks[].available | boolean | required | |
| blocks[].unavailable_reason | string | null | required | Why the block cannot be used, in one plain sentence an owner reads: the sentence validate refuses it with at save. null when available is true. |
| blocks[].outcomes | object[] | required | The named outcomes this block may end with besides working or failing, each with the causes it gives. A split block maps one to a branch ({ "type": "outcome", "step", "is", "cause" }). Empty for a block that only works or fails. |
| blocks[].outcomes[].name | string | required | |
| blocks[].outcomes[].description | string | required | |
| blocks[].outcomes[].causes | object[] | required | |
| blocks[].test_reads_for_real | object[] | required | The integration actions a Test runs for real through this block: reads, each marked so one by one. Every other integration action is held back in a Test, whatever it does. Empty for every block but “Call an integration”; any other block runs in a Test only when its side_effect is none. |
| blocks[].test_reads_for_real[].integration | string | required | |
| blocks[].test_reads_for_real[].action | string | required | |
| blocks[].test_reads_for_real[].title | string | required | |
| triggers | object[] | required | |
| triggers[].name | string | required | |
| triggers[].description | string | required | |
| triggers[].inputs | object | required | |
| triggers[].produced_by | string | null | required | |
| triggers[].available | boolean | required | |
| triggers[].params_schema | object | required | The JSON Schema of the trigger’s params: exactly the ones it accepts, each with a plain description, required naming the ones it needs, and the recipe forms’ picker where a param is one (tag, account, form, sheet, tab). On a start an integration delivers, account also has connectors: the keys of the integrations whose accounts it starts on, the ones switching on binds, those not available yet left out; so do form_id, spreadsheet_id and sheet, with the integrations whose trigger takes that setting. An empty object schema when it takes none. Validate refuses any other param, and a needed one left out, at /trigger/params/<name>. |
| triggers[].answers | boolean | required | Whether the trigger can carry answers (what of a customer’s message the automation claims). Only whatsapp.message_received can. |
| triggers[].filters | object[] | required | The checks on the event a builder offers on this start, each optional (“From contains”). Each is one clause of the trigger’s filters.only_when: clause, with the owner’s value as a lit where the param named name stands. Several are one { "kind": "op", "op": "and", "args": [...] }. They are decided before a run exists: an event they keep out is not run and counts nothing (its run is skipped, filtered). Only email.received has any: from_contains and subject_contains, matched in any case, and label, a Gmail label matched exactly (the owner’s by name, Gmail’s own by id, such as STARRED). Empty on every other trigger. |
| triggers[].filters[].name | string | required | |
| triggers[].filters[].title | string | required | |
| triggers[].filters[].description | string | required | |
| triggers[].filters[].clause | object | required | |
| integrations | object[] | required | Every integration that has actions, with them: what a “Call an integration” step can name as its integration and action, and what each takes and answers. |
| integrations[].integration | string | required | The integration’s key: what a “Call an integration” step names as integration. |
| integrations[].actions | object[] | required | |
| integrations[].actions[].action | string | required | The action’s key: what the step names as action. |
| integrations[].actions[].title | string | required | |
| integrations[].actions[].description | string | null | required | What the action does, in plain words. null when it declares none. |
| integrations[].actions[].input_schema | object | required | The JSON Schema of the step’s input: a field with a default may be left out. |
| integrations[].actions[].output_schema | object | null | required | The JSON Schema of what the action answers, which the step returns as result. null when the action does not declare it. |
| integrations[].actions[].side_effect | "none" | "internal_write" | "external_send" | required | |
| integrations[].actions[].destructive | boolean | required | Whether it deletes or cancels something that cannot be got back. A step that calls it is confirmed by the name integration.call:<integration>.<action> before the automation is switched on, and data a stranger controls may not decide whether it runs. |
| integrations[].actions[].test_reads_for_real | boolean | required | Whether a Test runs it for real: a read, marked so. Every other action is held back in a Test. |
/v1/automations
GET /v1/automations
The org’s automations, with today’s runs and who set each up. ?connection_id= (repeatable) lists those numbers’, ?slug= one kind
Scopes automations:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| connection_id | query | string | string[] | optional | WhatsApp numbers (connection ids), repeatable: the rows for any of them. Automations: those on it, and those made before numbers were required, which have none and run on every number. Runs: those that ran on it, and those that name no number of their own whose automation is one of these. |
| slug | query | string | optional | Only the automations with this slug, as GET /v1/recipes names a recipe: one per number it is set up on. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "automation" | required | |
| data[].slug | string | required | |
| data[].name | string | required | |
| data[].description | string | null | required | |
| data[].status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| data[].trigger_kind | string | required | |
| data[].trigger_config | object | required | |
| data[].connection_id | string | null | required | |
| data[].active_version_id | string | null | required | |
| data[].consecutive_failures | integer | required | |
| data[].last_run_at | string (date-time) | null | required | |
| data[].last_error | string | null | required | |
| data[].auto_disabled_at | string (date-time) | null | required | |
| data[].auto_disabled_reason | string | null | required | |
| data[].created_at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required | |
| data[].can_run | boolean | required | |
| data[].blocked_reason | string | null | required | |
| data[].activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| data[].activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| data[].activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| data[].activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| data[].template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| data[].template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| data[].template_approval.template | string | required | The template's system name, as the automation sends it. |
| data[].template_approval.message | string | required | What it means, in a sentence for the owner. |
| data[].active_version | any | optional | |
| data[].latest_version | any | optional | |
| data[].settings | object | optional | |
| data[].settings.version_id | string | required | |
| data[].settings.version_no | integer | required | |
| data[].settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| data[].settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| data[].settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| data[].authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| data[].conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| data[].conflicts[].automation_id | string | required | |
| data[].conflicts[].name | string | required | |
| data[].conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| data[].conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| data[].sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| data[].sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| data[].sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| data[].sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| data[].sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| data[].warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| data[].warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| data[].warnings[].message | string | required | What it means, in sentences for the owner. |
| data[].runs_today | integer | required | |
| data[].failures_today | integer | required | |
| data[].created_by | object | null | required | |
| data[].created_by.id | string | required | |
| data[].created_by.name | string | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/automations
Create an automation from an IR document. The automation is created as a draft
Scopes automations:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| slug | string | required | |
| name | string | required | |
| description | string | null | optional | |
| connection_id | string | required | The WhatsApp number (connection id) this automation belongs to: its triggers come from it, its messages go from it, and its templates are read on it. Required, and it cannot be moved afterwards. The same recipe (slug) can be set up once on each number, or once per source on it when the recipe has multiple. |
| ir | object | required | The IR document (packages/ir). Opaque on the wire by design: the engine internals are not part of the published contract. ir_version selects the document schema the server validates against, and a document that does not match is refused with automation_invalid, whose error.details[] names each failing path as a JSON pointer. |
| ir.ir_version | "1.0" | required | |
| change_note | string | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
Errors automation_invalid
GET /v1/automations/{id}
One automation. ?expand=active_version includes the deployed version; ?expand=latest_version the newest one; ?expand=settings the newest version’s recipe settings
Scopes automations:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| expand | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
PATCH /v1/automations/{id}
Metadata only. The logic is versioned, so changes to it and to quiet hours go through /versions
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | |
| description | string | null | optional | |
| connection_id | string | null | optional | The WhatsApp number of an automation made before numbers were required, which has none and runs on every number. Accepted once; an automation that has a number keeps it, and another one (or null) is refused. The value it already has, null included, changes nothing. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
DELETE /v1/automations/{id}
Delete an automation. No run starts after it, it leaves the list, its id answers 404
Scopes automations:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| deleted | true | required |
POST /v1/automations/{id}/copy
Make a custom copy of an automation, a recipe’s included: switched off, on the same number, with its own copy of the persona, knowledge and remembered values
Scopes automations:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | The copy’s name. Absent: the original’s, followed by “(copy)”. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
POST /v1/automations/{id}/deploy
Compile, run the artefact firewall, and store the artefact on the version
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| version_id | string | optional | |
| confirm_actions | string[] | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation_version" | required | |
| automation_id | string | required | |
| version_no | integer | required | |
| status | "draft" | "validated" | "deployed" | "superseded" | "rolled_back" | "failed" | required | |
| ir_hash | string | required | |
| ir | object | optional | |
| compiled_hash | string | null | required | |
| catalogue_version | string | required | |
| compiler_version | string | required | |
| derived_acts | string[] | required | |
| derived_budget | object | required | |
| segment_count | integer | required | |
| quiet_hours | object | null | required | |
| quiet_hours.start | string | required | |
| quiet_hours.end | string | required | |
| quiet_hours.policy | "shift_to_end" | "shift_to_start" | "skip" | "ignore" | "defer_unless_urgent" | required | |
| authored_by | "user" | "ai" | "recipe" | "system" | "import" | required | |
| change_note | string | null | required | |
| created_at | string (date-time) | required | |
| deployed_at | string (date-time) | null | required | |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
POST /v1/automations/{id}/disable
Stop an automation. Event triggers stop instantly, because the dispatcher reads its status
Scopes automations:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
POST /v1/automations/{id}/enable
Activate a version. For event triggers this is instant and atomic in our database
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| version_id | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
GET /v1/automations/{id}/knowledge
What this AI assistant knows: files, pasted text, websites and Google places. Nothing from another assistant
Scopes automations:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "knowledge_source" | required | |
| data[].kind | "file" | "text" | "url" | "place" | required | |
| data[].title | string | required | |
| data[].status | "queued" | "ready" | "failed" | required | |
| data[].error | string | null | required | |
| data[].media_id | string | null | required | |
| data[].url | string | null | required | |
| data[].page_count | integer | null | required | |
| data[].failed_pages | object[] | required | |
| data[].failed_pages[].url | string | required | |
| data[].failed_pages[].reason | string | required | |
| data[].failed_page_count | integer | required | |
| data[].bytes | integer | null | required | |
| data[].refreshed_at | string | null | required | |
| data[].refresh_every | "monthly" | null | required | |
| data[].created_at | string | required | |
| data[].updated_at | string | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/automations/{id}/knowledge
Give this AI assistant something to read: a file (med_…), text, a website or a Google Maps link. It is read in the background
Scopes automations:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| kind | "file" | required | |
| media_id | string | required | A file uploaded on this automation’s number with POST /v1/media. A file uploaded on another number is refused with media_other_number. |
| text | string | required | |
| title | string | optional | |
| url | string (uri) | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "knowledge_source" | required | |
| kind | "file" | "text" | "url" | "place" | required | |
| title | string | required | |
| status | "queued" | "ready" | "failed" | required | |
| error | string | null | required | |
| media_id | string | null | required | |
| url | string | null | required | |
| page_count | integer | null | required | |
| failed_pages | object[] | required | |
| failed_pages[].url | string | required | |
| failed_pages[].reason | string | required | |
| failed_page_count | integer | required | |
| bytes | integer | null | required | |
| refreshed_at | string | null | required | |
| refresh_every | "monthly" | null | required | |
| created_at | string | required | |
| updated_at | string | required |
Errors media_other_number
DELETE /v1/automations/{id}/knowledge/{sourceId}
Forget one of this AI assistant’s knowledge sources, and everything read from it
Scopes automations:write
Response 204
No content
POST /v1/automations/{id}/knowledge/{sourceId}/refresh
Read one of this AI assistant’s knowledge sources again now. A website is also read again every month
Scopes automations:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "knowledge_source" | required | |
| kind | "file" | "text" | "url" | "place" | required | |
| title | string | required | |
| status | "queued" | "ready" | "failed" | required | |
| error | string | null | required | |
| media_id | string | null | required | |
| url | string | null | required | |
| page_count | integer | null | required | |
| failed_pages | object[] | required | |
| failed_pages[].url | string | required | |
| failed_pages[].reason | string | required | |
| failed_page_count | integer | required | |
| bytes | integer | null | required | |
| refreshed_at | string | null | required | |
| refresh_every | "monthly" | null | required | |
| created_at | string | required | |
| updated_at | string | required |
PATCH /v1/automations/{id}/knowledge/suggestions/{suggestionId}
Accept one of this AI assistant’s suggested questions and answers, with your edits, or reject it. An accepted one becomes its knowledge
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| accepted | boolean | required | |
| question | string | optional | |
| answer | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "knowledge_suggestion" | required | |
| source_id | string | required | |
| question | string | required | |
| answer | string | required | |
| accepted | boolean | null | required | |
| accepted_source_id | string | null | required | |
| decided_at | string | null | required | |
| created_at | string | required |
GET /v1/automations/{id}/knowledge/summary
“What I learned” for this AI assistant: a short summary and suggested questions and answers to confirm
Scopes automations:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "knowledge_summary" | required | |
| status | "empty" | "learning" | "ready" | "failed" | required | |
| summary | string | null | required | |
| business_name | string | null | required | The business name, read from the material: at most 100 characters. Null when the material did not say. |
| what_you_do | string | null | required | One line about what the business does, read from the material: at most 200 characters. Null when the material did not say. |
| error | string | null | required | |
| generated_at | string | null | required | |
| suggestions | object[] | required | |
| suggestions[].id | string | required | |
| suggestions[].object | "knowledge_suggestion" | required | |
| suggestions[].source_id | string | required | |
| suggestions[].question | string | required | |
| suggestions[].answer | string | required | |
| suggestions[].accepted | boolean | null | required | |
| suggestions[].accepted_source_id | string | null | required | |
| suggestions[].decided_at | string | null | required | |
| suggestions[].created_at | string | required |
GET /v1/automations/{id}/persona
This AI assistant’s persona: the saved one, or the defaults
Scopes automations:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "ai_persona" | required | |
| business_name | string | required | |
| what_you_do | string | required | |
| tone | "friendly" | "neutral" | "formal" | required | |
| may_quote_prices | boolean | required | |
| links | object[] | required | |
| links[].url | string | required | |
| links[].when | string | required | |
| never_say | string[] | required | |
| confidence_threshold | number | required | |
| updated_at | string | null | required |
PATCH /v1/automations/{id}/persona
Change this AI assistant’s persona: send only the fields to change. Typed fields only; there is no free-text prompt
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| business_name | string | optional | |
| what_you_do | string | optional | |
| tone | "friendly" | "neutral" | "formal" | optional | |
| may_quote_prices | boolean | optional | |
| links | object[] | optional | |
| links[].url | string (uri) | required | |
| links[].when | string | required | |
| never_say | string[] | optional | |
| confidence_threshold | number | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "ai_persona" | required | |
| business_name | string | required | |
| what_you_do | string | required | |
| tone | "friendly" | "neutral" | "formal" | required | |
| may_quote_prices | boolean | required | |
| links | object[] | required | |
| links[].url | string | required | |
| links[].when | string | required | |
| never_say | string[] | required | |
| confidence_threshold | number | required | |
| updated_at | string | null | required |
POST /v1/automations/{id}/rollback
Point the automation back at an earlier version. A pointer move, not a rewrite
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| to_version | integer | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation" | required | |
| slug | string | required | |
| name | string | required | |
| description | string | null | required | |
| status | "draft" | "enabled" | "disabled" | "archived" | "auto_disabled" | required | |
| trigger_kind | string | required | |
| trigger_config | object | required | |
| connection_id | string | null | required | |
| active_version_id | string | null | required | |
| consecutive_failures | integer | required | |
| last_run_at | string (date-time) | null | required | |
| last_error | string | null | required | |
| auto_disabled_at | string (date-time) | null | required | |
| auto_disabled_reason | string | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| can_run | boolean | required | |
| blocked_reason | string | null | required | |
| activation | object | required | Whether this automation can run, as far as WhatsApp templates go: every template it sends must be approved and not paused on the number it sends from (every number when it names none), in the language the step names, else in any language. A template WhatsApp paused holds the whole automation, as one waiting for approval does. |
| activation.state | "active" | "pending_template_approvals" | required | active: no template this automation sends waits for WhatsApp (or it sends none). pending_template_approvals: one or more is not approved, or is paused by WhatsApp; no step runs for any trigger and nothing is sent. Each trigger ends its run skipped (skip_reason template_pending), which is not a failure, until every one is approved and not paused, and then it runs by itself. Read with status, which says whether it is switched on. |
| activation.message | string | null | required | What it means and what the owner does next, in sentences for the owner. null when active. |
| activation.templates | object[] | required | Every template send that is not approved, not_approved ones first. Empty when active. |
| activation.templates[].name | string | required | The template's system name, as the automation sends it. |
| activation.templates[].language | string | null | required | The language the step names; null when it names none, and any sendable language would do. |
| activation.templates[].display_name | string | required | The name the owner gave it, as the Templates page shows it. |
| activation.templates[].approval | "waiting" | "not_approved" | "paused" | required | waiting: WhatsApp is reviewing it (pending, or in appeal). not_approved: WhatsApp has not approved it (rejected, or any status but pending, in appeal, approved or paused), or the number does not have it; the owner changes it on the Templates page and submits it again, or chooses another. paused: WhatsApp paused it (its quality); the automation runs again by itself once WhatsApp un-pauses it. |
| template_approval | object | null | required | The first entry of activation.templates, kept for older clients: a template not approved is named before a pending one. null when activation.state is active. |
| template_approval.code | "template_pending" | "template_rejected" | required | template_pending: WhatsApp is reviewing the message, or paused it. template_rejected: WhatsApp did not approve it. Either way no step of the automation runs until every template it sends is approved and not paused (see activation). |
| template_approval.template | string | required | The template's system name, as the automation sends it. |
| template_approval.message | string | required | What it means, in a sentence for the owner. |
| active_version | any | optional | |
| latest_version | any | optional | |
| settings | object | optional | |
| settings.version_id | string | required | |
| settings.version_no | integer | required | |
| settings.recipe_slug | string | null | required | The recipe the version was made from (the IR’s recipe_id); null for an automation written by hand. |
| settings.params | object | null | required | The value of each setting the version declares, as chosen. A new value is saved as a new version (POST /v1/automations/{id}/versions) whose document gives it: for a recipe-made automation, the document POST /v1/recipes/{slug}/instantiate makes from the new values, which writes them into its steps; for one written by hand, its own document, whose steps name its settings. Either way the values are checked there against the version’s own settings, the same way. null when the version gives none. |
| settings.inputs_schema | object | null | required | The settings the version declares, as a JSON Schema to draw the form from. For a recipe-made automation it is the same as its card in GET /v1/recipes. required names each one it cannot run without. null when it declares none. |
| settings.inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| settings.inputs_schema.properties | object | optional | |
| authoring_conversation_id | string | null | optional | On GET /v1/automations/{id}: the AI authoring conversation that holds this automation (GET /v1/ai/authoring/{id}), or null when none does. An automation has at most one. |
| conflicts | object[] | optional | On POST /v1/automations/{id}/enable and GET /v1/automations/{id}: every other switched-on automation on its number that starts on the same thing: the same trigger, and the same tag, account, form, sheet, schedule or web address where it has one (one left out is every one); on incoming messages, every other one; on a schedule, also every other one whose steps use the same Google sheet, at whatever time it runs. Empty while this one is off. Only automations the caller can read are named. Nothing is refused or switched off: the owner decides. |
| conflicts[].automation_id | string | required | |
| conflicts[].name | string | required | |
| conflicts[].shares | string | required | What both start on, in words for the owner: "both answer incoming messages", "both start when a contact gets a tag (tag VIP)". |
| conflicts[].answers_first | boolean | null | required | On incoming messages, when both reply: whether this automation answers a customer’s message before that one, by the order the number offers a message in (the one the live order uses): a customer one of them is waiting on, then a keyword or an ad its start names (trigger.answers.starts_when), then one that answers every message; set up first among equals. One whose start claims no message (a keyword checked only inside its flow) comes after one that does. null when one of the two does not reply, or neither claims a message, so both run; and on every other trigger. |
| sheets | object[] | optional | On GET /v1/automations/{id}: each Google Sheet its steps add to, update or find rows in, once each, in step order. Read from the switched-on version, or the newest when none is switched on. Empty when no step uses one. |
| sheets[].spreadsheet_id | string | required | Google’s id for the spreadsheet. |
| sheets[].title | string | null | required | The name Google gave it when Cosend made it. null for a spreadsheet the owner picked, which Cosend did not make. |
| sheets[].tab | string | null | required | The tab its steps’ ranges name before a ! (the first one named). null when none names one: they use the spreadsheet’s first tab. |
| sheets[].url | string | null | required | Google’s link to open it, as Google gave it when Cosend made it. null for a spreadsheet the owner picked. |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
GET /v1/automations/{id}/stats
How one automation did today or over seven days: chats answered alone, handed over, and why
Scopes automations:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| range | query | "today" | "7d" | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| range | "today" | "7d" | required | |
| answered_alone | integer | required | |
| handed_over | integer | required | |
| days | object[] | required | |
| days[].date | string (date) | required | |
| days[].answered_alone | integer | required | |
| days[].handed_over | integer | required | |
| top_handover_reasons | object[] | required | |
| top_handover_reasons[].code | string | required | |
| top_handover_reasons[].n | integer | required |
POST /v1/automations/{id}/test
Run the newest version once on a sample customer, sending to no one; each step is recorded as what it would have done
Scopes runs:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| payload | object | optional | Optional. Trigger subtrees (message, order, …) laid over the sample's, field by field. |
| fail_step | string | optional | Optional. The id of a step: the test runs the error branch as if that step had failed, and shows what the branch would do. Refused when the automation has no error branch or no such step. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "run" | required | |
| automation_id | string | required | |
| automation_name | string | required | The name its owner knows the automation by: for one installed with another, that one’s name, and a deleted automation keeps its own. So a run is named even when its automation is not in the automations list. |
| connection_id | string | null | required | |
| version_no | integer | required | |
| status | "queued" | "running" | "waiting" | "success" | "failure" | "cancelled" | "timeout" | "skipped" | … | required | |
| skip_reason | "quota_exceeded" | "ai_capped" | "bot_disabled" | "outside_service_window" | "opted_out" | "connection_not_active" | "concurrency_limit" | "duplicate" | … | null | required | |
| trigger | object | required | |
| trigger.kind | string | required | |
| trigger.message_id | string | null | required | |
| started_at | string (date-time) | null | required | |
| finished_at | string (date-time) | null | required | |
| duration_ms | integer | null | required | |
| steps_failed | integer | required | |
| usage | object | required | |
| usage.run_units | integer | required | |
| usage.ai_conversation_id | string | null | required | |
| usage.external_sends | integer | required | |
| sent_messages | object[] | required | The WhatsApp messages this run sent, in the order sent: the first ten, each with whether WhatsApp accepted it (delivery). Empty for a test run, which sends nothing. |
| sent_messages[].message_id | string | required | |
| sent_messages[].type | "template" | "text" | "interactive" | "image" | "document" | "audio" | "video" | "sticker" | required | What was sent. A template names its template in template; every other kind is a message written in the automation, and its template is null. |
| sent_messages[].template | object | null | required | |
| sent_messages[].template.id | string | required | |
| sent_messages[].template.display_name | string | required | The name the owner gave the template, as the Templates page shows it. |
| sent_messages[].to | object | required | |
| sent_messages[].to.contact_id | string | required | |
| sent_messages[].to.name | string | null | required | The customer's name as the Inbox shows it; null when they have none. |
| sent_messages[].to.phone | string | null | required | Their number in E.164; null when they are known only by their WhatsApp id. |
| sent_messages[].delivery | object | required | |
| sent_messages[].delivery.state | "sending" | "sent" | "not_sent" | required | sent once WhatsApp accepted the message; sending while it is still on its way; not_sent when it was stopped: the customer opted out, the owner answered from the WhatsApp Business app, or WhatsApp refused it. |
| sent_messages[].delivery.reason | string | null | required | Why it was not sent, in plain words. Null unless state is not_sent. |
| sent_messages[].delivery.detail | string | null | required | WhatsApp’s or Cosend’s own text for a message that was not sent, for developers. Null otherwise. |
| result | any | null | required | |
| error | any | null | required | |
| failed_step | object | null | required | |
| failed_step.step_id | string | null | required | |
| failed_step.step_action | string | null | required | |
| failed_step.error | object | null | required | |
| failed_step.error.code | string | required | |
| failed_step.error.message | string | required | What went wrong, in plain words. |
| failed_step.error.detail | string | null | required | The technical text, in full, for Details. Null when the message is all there is, and for an error on our side. |
| failure | object | null | required | |
| failure.reason | string | required | Why the run failed, in plain words. |
| failure.detail | string | null | required | The technical text the reason was said in place of, in full, for Details. Null when the reason is all there is. |
| failure.step_id | string | null | required | The step it stopped at; null when no step failed. |
| failure.step_action | string | null | required | |
| failure.before_first_step | boolean | required | True when the run stopped before it recorded any step. |
| executor_error | object | null | required | Windmill’s own account of why it failed or cancelled this run: the step, its error and the end of its log. name and message are null when Windmill gave no reason. Null for a run Windmill did not report failed or cancelled. |
| executor_error.job_id | string | required | The Windmill job the error was read from: the failed step's, or the run's own. |
| executor_error.step_id | string | null | required | The flow step Windmill marked failed; null when none did. |
| executor_error.step_action | string | null | required | That step’s catalogue action, when it maps to one. |
| executor_error.name | string | null | required | Windmill’s error name. |
| executor_error.message | string | null | required | Windmill’s error message, at most 1000 characters, with secrets removed. |
| executor_error.log_tail | string | null | required | The last lines of that step’s log, at most 20 lines and 2000 characters, with secrets removed. Null when Windmill gave none. |
| test | object | null | required | |
| test.customer | object | null | required | |
| test.customer.name | string | required | |
| test.customer.phone | string | required | |
| test.trigger_type | string | null | required | |
| test.payload | object | required |
POST /v1/automations/{id}/validate
Validate and report, without deploying. Called on every keystroke-debounce
Scopes automations:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| ir | object | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| valid | boolean | required | |
| compiler_version | string | required | |
| catalogue_version | string | required | |
| errors | object[] | required | |
| errors[].pointer | string | required | |
| errors[].code | string | required | |
| errors[].message | string | required | |
| errors[].fix | object | optional | |
| errors[].fix.action | string | required | |
| errors[].fix.url | string | optional | |
| errors[].detail | string | optional | When message is in plain words for the owner, the technical text it was said in place of, for Details. |
| warnings | object[] | required | |
| warnings[].pointer | string | required | |
| warnings[].code | string | required | |
| warnings[].message | string | required | |
| warnings[].fix | object | optional | |
| warnings[].fix.action | string | required | |
| warnings[].fix.url | string | optional | |
| warnings[].detail | string | optional | When message is in plain words for the owner, the technical text it was said in place of, for Details. |
| derived | object | optional | |
| derived.acts | string[] | required | |
| derived.budget | object | required | |
| derived.budget.actions | integer | required | |
| derived.budget.ai_calls | integer | required | |
| derived.budget.external_sends | integer | required | |
| derived.segment_count | integer | required |
GET /v1/automations/{id}/versions
Every version of an automation, newest first. Append-only
Scopes automations:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "automation_version" | required | |
| data[].automation_id | string | required | |
| data[].version_no | integer | required | |
| data[].status | "draft" | "validated" | "deployed" | "superseded" | "rolled_back" | "failed" | required | |
| data[].ir_hash | string | required | |
| data[].ir | object | optional | |
| data[].compiled_hash | string | null | required | |
| data[].catalogue_version | string | required | |
| data[].compiler_version | string | required | |
| data[].derived_acts | string[] | required | |
| data[].derived_budget | object | required | |
| data[].segment_count | integer | required | |
| data[].quiet_hours | object | null | required | |
| data[].quiet_hours.start | string | required | |
| data[].quiet_hours.end | string | required | |
| data[].quiet_hours.policy | "shift_to_end" | "shift_to_start" | "skip" | "ignore" | "defer_unless_urgent" | required | |
| data[].authored_by | "user" | "ai" | "recipe" | "system" | "import" | required | |
| data[].change_note | string | null | required | |
| data[].created_at | string (date-time) | required | |
| data[].deployed_at | string (date-time) | null | required | |
| data[].warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| data[].warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| data[].warnings[].message | string | required | What it means, in sentences for the owner. |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/automations/{id}/versions
A new version from an IR document. Validated; new settings for a switched-on automation are also built and switched on
Scopes automations:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| ir | object | required | |
| change_note | string | null | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "automation_version" | required | |
| automation_id | string | required | |
| version_no | integer | required | |
| status | "draft" | "validated" | "deployed" | "superseded" | "rolled_back" | "failed" | required | |
| ir_hash | string | required | |
| ir | object | optional | |
| compiled_hash | string | null | required | |
| catalogue_version | string | required | |
| compiler_version | string | required | |
| derived_acts | string[] | required | |
| derived_budget | object | required | |
| segment_count | integer | required | |
| quiet_hours | object | null | required | |
| quiet_hours.start | string | required | |
| quiet_hours.end | string | required | |
| quiet_hours.policy | "shift_to_end" | "shift_to_start" | "skip" | "ignore" | "defer_unless_urgent" | required | |
| authored_by | "user" | "ai" | "recipe" | "system" | "import" | required | |
| change_note | string | null | required | |
| created_at | string (date-time) | required | |
| deployed_at | string (date-time) | null | required | |
| warnings | object[] | optional | Present only on the answer to the call that switched it on, when something about it needs saying. The automation is on: it is a note, never an error. |
| warnings[].code | "catch_all_shadowed" | required | catch_all_shadowed: two automations on this number answer every message. The one set up first answers; the other answers only taps on buttons it sent and customers it is waiting on, while the first is on. |
| warnings[].message | string | required | What it means, in sentences for the owner. |
/v1/billing
POST /v1/billing/card-setup
On the Free plan, open a Stripe page that saves a card and requires the billing address
Scopes billing:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| return_url | string (uri) | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | required |
Errors invalid_request
POST /v1/billing/checkout
Start a Stripe Checkout session for a plan change
Scopes billing:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| plan_code | string | required | |
| interval | "month" | "year" | optional | |
| return_url | string (uri) | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | required |
POST /v1/billing/checkout/embedded
Start a Stripe Checkout session that renders inside our own dashboard
Scopes billing:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| plan_code | string | required | |
| interval | "month" | "year" | optional | |
| return_url | string (uri) | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| client_secret | string | required |
POST /v1/billing/downgrade-consent
Record a customer’s typed approval of the data loss a downgrade causes
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| to_plan_code | string | required | |
| typed_approval | string | required | |
| typed_name | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| from_plan_code | string | required | |
| to_plan_code | string | required | |
| typed_approval | string | required | |
| typed_name | string | required | |
| actor_user_id | string | null | required | |
| actor_label | string | required | |
| created_at | string | required |
GET /v1/billing/invoices
The organization’s invoices: when, how much, paid, and Stripe’s own document
Scopes billing:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].number | string | null | required | |
| data[].created_at | string (date-time) | required | |
| data[].status | string | required | |
| data[].paid | boolean | required | |
| data[].amount_due_minor | integer | required | |
| data[].amount_paid_minor | integer | required | |
| data[].currency | string | required | |
| data[].hosted_invoice_url | string (uri) | null | required | |
| data[].invoice_pdf | string (uri) | null | required | |
| next_invoice | object | null | required | |
| next_invoice.amount_due_minor | integer | required | |
| next_invoice.currency | string | required | |
| next_invoice.charged_at | string (date-time) | null | required |
POST /v1/billing/invoices/{id}/pay
Pay one of the organization’s open invoices now, on the card on file
Scopes billing:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| invoice_id | string | required | |
| status | "paid" | required | |
| subscription_status | "trialing" | "active" | "past_due" | "unpaid" | "canceled" | "incomplete" | required | |
| message | string | required |
Errors invalid_request payment_declined upstream_unavailable
GET /v1/billing/invoices/{invoiceId}
One of the organization’s own invoices, by Stripe id
Scopes billing:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| number | string | null | required | |
| created_at | string (date-time) | required | |
| status | string | required | |
| paid | boolean | required | |
| amount_due_minor | integer | required | |
| amount_paid_minor | integer | required | |
| currency | string | required | |
| hosted_invoice_url | string (uri) | null | required | |
| invoice_pdf | string (uri) | null | required |
POST /v1/billing/plan-change
Change the plan on the live subscription in place, on the card on file
Scopes billing:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| plan_code | string | required | |
| interval | "month" | "year" | optional | |
| proration_date | string (date-time) | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| plan_code | string | required | |
| plan | object | required | |
| plan.code | string | required | |
| plan.name | string | required | |
| plan.rank | integer | required | |
| interval | "month" | "year" | required | |
| plan_change_mode | "checkout" | "in_place" | required | |
| downgrade_consent_word | string | required | |
| status | "trialing" | "active" | "past_due" | "unpaid" | "canceled" | "incomplete" | required | |
| overdue | boolean | required | |
| overdue_since | string (date-time) | null | required | |
| current_period_end | string (date-time) | required | |
| cancel_at_period_end | boolean | required | |
| cancel_to_plan | "free" | null | required | |
| scheduled_change | object | null | required | |
| scheduled_change.plan_code | string | required | |
| scheduled_change.interval | "month" | "year" | required | |
| scheduled_change.effective_at | string (date-time) | required | |
| scheduled_change.limits_lowered | object[] | required | |
| scheduled_change.limits_lowered[].limit | "history_months" | "team_members" | "connections" | "messages" | "ai_conversations" | "automation_runs" | required | |
| scheduled_change.limits_lowered[].from | integer | null | required | |
| scheduled_change.limits_lowered[].to | integer | null | required | |
| scheduled_change.limits_lowered[].to_basis | "period" | "lifetime" | "concurrent" | null | required | |
| scheduled_change.members_losing_access | integer | null | required | |
| scheduled_change.reminder_due | boolean | required | |
| scheduled_change.created_at | string (date-time) | null | required | |
| canceled_at | string (date-time) | null | required | |
| trial_ends_at | string (date-time) | null | required | |
| trial | null | required | |
| seats_used | integer | required | |
| seats_included | integer | null | required | |
| seats_hard_cap | integer | null | required | |
| billing_information | "unknown" | "present" | "absent" | required | |
| billing_information_updated_at | string (date-time) | null | required | |
| usage_billing_problem | string | null | required |
POST /v1/billing/plan-change/preview
Preview a plan change on the live subscription: the next invoice, and whether a consent is needed
Scopes billing:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| plan_code | string | required | |
| interval | "month" | "year" | optional | |
| proration_date | string (date-time) | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| plan_code | string | required | |
| interval | "month" | "year" | required | |
| current_plan_code | string | required | |
| current_interval | "month" | "year" | required | |
| direction | "upgrade" | "downgrade" | "interval_only" | "same_plan" | required | |
| downgrade_consent | "not_required" | "required" | "recorded" | required | |
| next_invoice | object | required | |
| next_invoice.amount_due_minor | integer | required | |
| next_invoice.currency | string | required | |
| next_invoice.charged_at | string (date-time) | null | required | |
| new_plan | object | required | |
| new_plan.code | string | required | |
| new_plan.interval | "month" | "year" | required | |
| new_plan.price_minor | integer | required | |
| credit_minor | integer | required | |
| charged_today_minor | integer | required | |
| earlier_changes_minor | integer | required | |
| recurring_minor | integer | required | |
| renewal_date | string (date-time) | required | |
| proration_date | string (date-time) | required | |
| lines | object[] | required | |
| lines[].description | string | null | required | |
| lines[].amount_minor | integer | required | |
| lines[].proration | boolean | required | |
| retention_lowered_to_months | integer | null | required | |
| scheduled_end_removed | boolean | required | |
| effective_at | string (date-time) | null | required | |
| scheduled_change_removed | boolean | required | |
| to_free | object | optional | |
| to_free.starts_at | string (date-time) | required | |
| to_free.members_losing_access | integer | required | |
| to_free.ai_conversations_used | integer | required |
GET /v1/billing/plans
The plans this account may move to, in ladder order: prices, allowances, seats and features
Scopes billing:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].code | string | required | |
| data[].name | string | required | |
| data[].rank | integer | required | |
| data[].prices | object | required | |
| data[].prices.monthly_minor | integer | required | |
| data[].prices.annual_minor | integer | required | |
| data[].prices.currency | string | required | |
| data[].included | object | required | |
| data[].basis | object | required | |
| data[].overage | object | required | |
| data[].hard_caps | object | required | |
| data[].seat_policy | object | required | |
| data[].seat_policy.included | integer | null | required | |
| data[].seat_policy.next_seat_price_minor | string | null | required | |
| data[].features | string[] | required |
POST /v1/billing/portal
Open the Stripe Customer Portal: cards and invoices, without cancellation or plan switching
Scopes billing:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| return_url | string (uri) | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | required |
GET /v1/billing/subscription
The current plan, status, period and overdue state
Scopes billing:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| plan_code | string | required | |
| plan | object | required | |
| plan.code | string | required | |
| plan.name | string | required | |
| plan.rank | integer | required | |
| interval | "month" | "year" | required | |
| plan_change_mode | "checkout" | "in_place" | required | |
| downgrade_consent_word | string | required | |
| status | "trialing" | "active" | "past_due" | "unpaid" | "canceled" | "incomplete" | required | |
| overdue | boolean | required | |
| overdue_since | string (date-time) | null | required | |
| current_period_end | string (date-time) | required | |
| cancel_at_period_end | boolean | required | |
| cancel_to_plan | "free" | null | required | |
| scheduled_change | object | null | required | |
| scheduled_change.plan_code | string | required | |
| scheduled_change.interval | "month" | "year" | required | |
| scheduled_change.effective_at | string (date-time) | required | |
| scheduled_change.limits_lowered | object[] | required | |
| scheduled_change.limits_lowered[].limit | "history_months" | "team_members" | "connections" | "messages" | "ai_conversations" | "automation_runs" | required | |
| scheduled_change.limits_lowered[].from | integer | null | required | |
| scheduled_change.limits_lowered[].to | integer | null | required | |
| scheduled_change.limits_lowered[].to_basis | "period" | "lifetime" | "concurrent" | null | required | |
| scheduled_change.members_losing_access | integer | null | required | |
| scheduled_change.reminder_due | boolean | required | |
| scheduled_change.created_at | string (date-time) | null | required | |
| canceled_at | string (date-time) | null | required | |
| trial_ends_at | string (date-time) | null | required | |
| trial | null | required | |
| seats_used | integer | required | |
| seats_included | integer | null | required | |
| seats_hard_cap | integer | null | required | |
| billing_information | "unknown" | "present" | "absent" | required | |
| billing_information_updated_at | string (date-time) | null | required | |
| usage_billing_problem | string | null | required |
/v1/connections
GET /v1/connections
List connections, with health at a glance
Scopes connections:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| status | query | string | optional | |
| mode | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "connection" | required | |
| data[].phone_number_id | string | required | |
| data[].waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| data[].meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| data[].display_phone_number | string | required | |
| data[].phone_e164 | string | required | |
| data[].verified_name | string | null | required | |
| data[].mode | "coexistence" | "cloud_api" | required | |
| data[].status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| data[].status_reason | string | null | required | |
| data[].status_message | string | null | required | |
| data[].token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| data[].throughput_mps | integer | required | |
| data[].quality_rating | string | null | required | |
| data[].quality_rating_previous | string | null | required | |
| data[].quality_changed_at | string (date-time) | null | required | |
| data[].messaging_limit | string | null | required | |
| data[].meta_business_id | string | null | required | |
| data[].last_app_activity_at | string (date-time) | null | required | |
| data[].delivery_mode | "stored" | "direct" | required | |
| data[].import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| data[].override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| data[].webhook | object | required | |
| data[].webhook.meta | object | null | required | |
| data[].webhook.verify_token | string | null | required | |
| data[].webhook.relay_secret | string | null | required | |
| data[].webhook.relay | object | required | |
| data[].is_test | boolean | required | |
| data[].connected_at | string (date-time) | null | required | |
| data[].registered_at | string (date-time) | null | required | |
| data[].register_deadline_at | string (date-time) | null | required | |
| data[].register_blocked_until | string (date-time) | null | required | |
| data[].display_name_status | string | null | required | |
| data[].display_name | object | required | |
| data[].display_name.current | string | null | required | |
| data[].display_name.requested | string | null | required | |
| data[].display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| data[].display_name.applied | boolean | null | required | |
| data[].display_name.reason | string | null | required | |
| data[].display_name.updated_at | string (date-time) | null | required | |
| data[].display_name.meta_status | string | null | required | |
| data[].display_name.meta_status_since | string (date-time) | null | required | |
| data[].display_name.meta_review | string | null | required | |
| data[].display_name.meta_review_since | string (date-time) | null | required | |
| data[].display_name.read_at | string (date-time) | null | required | |
| data[].display_name.decision | object | null | required | |
| data[].display_name.refusal | object | null | required | |
| data[].display_name.applied_at | string (date-time) | null | required | |
| data[].display_name.apply_by | string (date-time) | null | required | |
| data[].display_name.submitted | object | null | required | |
| data[].display_name.submissions_in_30_days | integer | required | |
| data[].display_name.next | object | required | |
| data[].templates_synced_at | string (date-time) | null | required | |
| data[].health | object | null | required | |
| data[].health.checked_at | string (date-time) | required | |
| data[].health.can_send_message | string | null | required | |
| data[].health.read_at | string (date-time) | null | required | |
| data[].health.entities | object[] | required | |
| data[].health.token | object | null | required | |
| data[].health.subscription | object | null | required | |
| data[].health.can_send | object | null | required | |
| data[].sending | object | required | |
| data[].sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| data[].sending.checked_at | string (date-time) | null | required | |
| data[].sending.problems | object[] | required | |
| data[].business_verification | object | required | |
| data[].business_verification.status | string | null | required | |
| data[].business_verification.since | string (date-time) | null | required | |
| data[].business_verification.checked_at | string (date-time) | null | required | |
| data[].business_verification.business_id | string | null | required | |
| data[].business_verification.business_name | string | null | required | |
| data[].business_verification.messaging_limit | string | null | required | |
| data[].business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| data[].business_verification.meta | object | null | required | |
| data[].business_verification.alerts | object[] | required | |
| data[].business_verification.delay | string | null | required | |
| data[].onboarding | object | null | required | |
| data[].onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| data[].onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| data[].onboarding.progress_pct | integer | required | |
| data[].onboarding.messages_synced | integer | required | |
| data[].onboarding.contacts_synced | integer | required | |
| data[].onboarding.sync_deadline_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
GET /v1/connections/{id}
One connection, with its onboarding summary
Scopes connections:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
PATCH /v1/connections/{id}
The two settings that change behaviour
Scopes connections:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| delivery_mode | "stored" | "direct" | optional | |
| retention_months | any | optional | Retention is one account-wide setting and is no longer configured per connection. Set it with PATCH /v1/account/data-policy. |
| override_callback_url | string (uri) | null | optional | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
DELETE /v1/connections/{id}
The supported disconnect: offboard at Meta, then soft-delete
Scopes connections:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| status | "offboarded" | required | |
| deleted_at | string (date-time) | required | |
| waba_unsubscribed | boolean | required | |
| meta_cleanup | "done" | "not_needed" | "skipped_access_lost" | required |
POST /v1/connections/{id}/display-name
Submit a new WhatsApp display name to Meta for review
Scopes connections:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| display_name | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
GET /v1/connections/{id}/onboarding
History-import progress, as polled by an onboarding screen
Scopes connections:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "onboarding" | required | |
| connection_id | string | required | |
| status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| progress_pct | integer | required | |
| contacts_synced | integer | required | |
| messages_synced | integer | required | |
| threads_synced | integer | required | |
| media_total | integer | required | |
| media_stored | integer | required | |
| media_unavailable | integer | required | |
| contacts_requested_at | string (date-time) | null | required | |
| contacts_completed_at | string (date-time) | null | required | |
| history_requested_at | string (date-time) | null | required | |
| history_completed_at | string (date-time) | null | required | |
| onboarded_at | string (date-time) | required | |
| sync_deadline_at | string (date-time) | required | |
| media_window_ends_at | string (date-time) | required | |
| history_horizon_at | string (date-time) | required | |
| error | object | null | required | |
| error.meta_code | integer | null | required | |
| error.reason | string | required | |
| error.message | string | required |
POST /v1/connections/{id}/payment-check
A payment method was added in Meta: a number blocked for payment sends again
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
GET /v1/connections/{id}/profile
The WhatsApp business profile customers see, read from Meta
Scopes connections:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| about | string | null | required | |
| description | string | null | required | |
| address | string | null | required | |
| string | null | required | ||
| websites | string[] | required | |
| vertical | "alcohol" | "apparel" | "automotive" | "beauty" | "education" | "entertainment" | "event_planning" | "finance" | … | null | required | |
| vertical_meta_value | string | null | required | |
| profile_picture_url | string | null | required | |
| read_only_reason | string | null | required |
PATCH /v1/connections/{id}/profile
Edit the WhatsApp business profile: about, description, address, email, websites, industry, picture
Scopes connections:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| about | string | optional | |
| description | string | null | optional | |
| address | string | null | optional | |
| string | null | optional | ||
| websites | string (uri)[] | optional | |
| vertical | "alcohol" | "apparel" | "automotive" | "beauty" | "education" | "entertainment" | "event_planning" | "finance" | … | null | optional | |
| profile_picture | object | optional | |
| profile_picture.media_id | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| about | string | null | required | |
| description | string | null | required | |
| address | string | null | required | |
| string | null | required | ||
| websites | string[] | required | |
| vertical | "alcohol" | "apparel" | "automotive" | "beauty" | "education" | "entertainment" | "event_planning" | "finance" | … | null | required | |
| vertical_meta_value | string | null | required | |
| profile_picture_url | string | null | required | |
| read_only_reason | string | null | required |
POST /v1/connections/{id}/reauth
The URL and Embedded Signup parameters for re-running the flow
Scopes connections:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| url | string | required | |
| config_id | string | required |
GET /v1/connections/{id}/reply-order
The order a WhatsApp number’s automations answer a customer’s message in. Read-only: change the automations to change it
Scopes automations:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| order | object[] | required | The switched-on automations on this number that answer a customer’s message, in the order a message is offered to them: by step, then the one set up first. Automations that do not answer (they tag, file a row) are not listed. |
| order[].step | "tapped" | "waiting" | "claims" | "every_message" | required | Where in the order it answers, from what it declares. tapped: it declares nothing, so it answers a tap on a button it sent, and a message no automation on the number answers. waiting: a customer it is waiting on for an answer. claims: a message its start condition names (its claims, which may be empty when the words cannot be read off it); it also answers a customer it is waiting on. every_message: any message. A tap always goes to the automation that sent the button, and a customer one is waiting on to that one, before the later steps. |
| order[].automation_id | string | required | |
| order[].name | string | required | |
| order[].claims | string[] | required | On step claims: each is a keyword, in lower case, or ad:<ad id> for the Click-to-WhatsApp ad a message came from; ad:* is any ad. Empty on the other steps. |
| order[].answers | true | required | |
| order[].shadowed_by | string | null | required | On step every_message: the automation that answers every message instead of this one, which is the first one set up. null for that first one and on the other steps. |
| updated_from | "automations" | required |
POST /v1/connections/{id}/sync
Refresh number state, quality and limits from Meta
Scopes connections:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
GET /v1/connections/{id}/timeline
The steps of this connection’s connect, with times and outcomes
Scopes connections:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "connect_timeline" | required | |
| attempt_id | string | required | |
| connection_id | string | null | required | |
| onboarding_id | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| attempt_status | "connecting" | "connected" | "needs_attention" | required | |
| attempt_step | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | "sync" | "done" | required | |
| onboarding_status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | null | required | |
| onboarded_at | string (date-time) | null | required | |
| sync_deadline_at | string (date-time) | null | required | |
| media_total | integer | null | required | |
| media_stored | integer | null | required | |
| media_unavailable | integer | null | required | |
| steps | object[] | required | |
| steps[].key | "exchange" | "saved" | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | … | required | |
| steps[].title | string | required | |
| steps[].state | "done" | "active" | "waiting" | "stopped" | "failed" | "skipped" | "passed" | required | |
| steps[].at | string (date-time) | null | required | |
| steps[].due_at | string (date-time) | null | required | |
| steps[].armed | boolean | null | required | |
| steps[].outcome | string | null | required | |
| steps[].log_event | string | null | required | |
| steps[].meta_request_id | string | null | required | |
| steps[].counts | object | null | required | |
| steps[].counts.chunks_received | integer | required | |
| steps[].counts.chunks_applied | integer | required | |
| steps[].counts.chunks_failed | integer | required | |
| steps[].counts.messages | integer | required | |
| steps[].counts.last_received_at | string (date-time) | null | required |
POST /v1/connections/{id}/two-step-pin
Set a new two-step verification PIN for a number Cosend registered
Scopes connections:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| pin | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
GET /v1/connections/attempts/{id}
One connect attempt, as polled by a connect screen
Scopes connections:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connect_attempt" | required | |
| status | "connecting" | "connected" | "needs_attention" | required | |
| step | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | "sync" | "done" | required | |
| connection_id | string | null | required | |
| waba_id | string | null | required | The WhatsApp Business Account, as Cosend's wab_ id (the waba_id its connection carries), from the step that saves the number on; null before. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | null | required | Meta's id for the WhatsApp Business Account, as Embedded Signup and Meta's Graph API give it; null until Meta has named it. Cosend's id for it is waba_id. |
| phone_number_id | string | null | required | |
| error | object | null | required | |
| error.code | string | required | upstream_unavailable while retrying. When stopped, one of permission_denied, number_in_use, no_number, display_name_rejected, payment_missing or unknown for Meta's answer, or Cosend's own code: connection_exists_elsewhere (another workspace has the number), plan_does_not_include, two_step_pin_required, number_move_refused, rate_limited, or invalid_request with candidates. |
| error.meta_code | integer | null | required | |
| error.message | string | required | |
| error.candidates | object[] | null | required | |
| error.candidates[].phone_number_id | string | required | |
| error.candidates[].display_phone_number | string | null | required | |
| error.candidates[].verified_name | string | null | required | |
| error.candidates[].platform_type | string | null | required | |
| error.candidates[].is_on_biz_app | boolean | null | required | |
| error.customer_step | "turn_off_two_step_verification" | "add_payment_method" | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
Errors connection_exists_elsewhere invalid_request number_move_refused plan_does_not_include rate_limited two_step_pin_required upstream_unavailable
POST /v1/connections/attempts/{id}/resume
Finish a connect attempt from where it stopped, without Meta’s window
Scopes connections:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| phone_number_id | string | null | optional | |
| two_step_pin | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connect_attempt" | required | |
| status | "connecting" | "connected" | "needs_attention" | required | |
| step | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | "sync" | "done" | required | |
| connection_id | string | null | required | |
| waba_id | string | null | required | The WhatsApp Business Account, as Cosend's wab_ id (the waba_id its connection carries), from the step that saves the number on; null before. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | null | required | Meta's id for the WhatsApp Business Account, as Embedded Signup and Meta's Graph API give it; null until Meta has named it. Cosend's id for it is waba_id. |
| phone_number_id | string | null | required | |
| error | object | null | required | |
| error.code | string | required | upstream_unavailable while retrying. When stopped, one of permission_denied, number_in_use, no_number, display_name_rejected, payment_missing or unknown for Meta's answer, or Cosend's own code: connection_exists_elsewhere (another workspace has the number), plan_does_not_include, two_step_pin_required, number_move_refused, rate_limited, or invalid_request with candidates. |
| error.meta_code | integer | null | required | |
| error.message | string | required | |
| error.candidates | object[] | null | required | |
| error.candidates[].phone_number_id | string | required | |
| error.candidates[].display_phone_number | string | null | required | |
| error.candidates[].verified_name | string | null | required | |
| error.candidates[].platform_type | string | null | required | |
| error.candidates[].is_on_biz_app | boolean | null | required | |
| error.customer_step | "turn_off_two_step_verification" | "add_payment_method" | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
Errors connection_exists_elsewhere invalid_request number_move_refused plan_does_not_include rate_limited two_step_pin_required upstream_unavailable
GET /v1/connections/attempts/{id}/timeline
The steps of one connect attempt, with times and outcomes
Scopes connections:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "connect_timeline" | required | |
| attempt_id | string | required | |
| connection_id | string | null | required | |
| onboarding_id | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| attempt_status | "connecting" | "connected" | "needs_attention" | required | |
| attempt_step | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | "sync" | "done" | required | |
| onboarding_status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | null | required | |
| onboarded_at | string (date-time) | null | required | |
| sync_deadline_at | string (date-time) | null | required | |
| media_total | integer | null | required | |
| media_stored | integer | null | required | |
| media_unavailable | integer | null | required | |
| steps | object[] | required | |
| steps[].key | "exchange" | "saved" | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | … | required | |
| steps[].title | string | required | |
| steps[].state | "done" | "active" | "waiting" | "stopped" | "failed" | "skipped" | "passed" | required | |
| steps[].at | string (date-time) | null | required | |
| steps[].due_at | string (date-time) | null | required | |
| steps[].armed | boolean | null | required | |
| steps[].outcome | string | null | required | |
| steps[].log_event | string | null | required | |
| steps[].meta_request_id | string | null | required | |
| steps[].counts | object | null | required | |
| steps[].counts.chunks_received | integer | required | |
| steps[].counts.chunks_applied | integer | required | |
| steps[].counts.chunks_failed | integer | required | |
| steps[].counts.messages | integer | required | |
| steps[].counts.last_received_at | string (date-time) | null | required |
POST /v1/connections/connect
Exchange an Embedded Signup code and connect the number
Scopes connections:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | required | |
| session | object | optional | |
| session.waba_id | string | optional | Meta's id for the WhatsApp Business Account, as Embedded Signup's finish event gave it, forwarded unchanged. It becomes the connection's meta_waba_id; the connection's waba_id is Cosend's wab_ id. |
| session.phone_number_id | string | optional | |
| session.business_id | string | optional | |
| session.session_id | string | optional | |
| session.event | string | optional | |
| delivery_mode | "stored" | "direct" | optional | |
| mode | "coexistence" | "cloud_api" | optional | |
| two_step_pin | string | optional | |
| moving_from_provider | boolean | optional | |
| import_history | boolean | optional | Import the chat history from the WhatsApp Business app. Default true. false: Cosend never asks WhatsApp for it, and the onboarding completes without it. |
| import_contacts | boolean | optional | Import contacts from the phone's address book. Default true. false: Cosend never asks WhatsApp for them and stores no address-book change WhatsApp sends later. Set only here. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connection" | required | |
| phone_number_id | string | required | |
| waba_id | string | required | The WhatsApp Business Account this number is on, as Cosend's wab_ id: the waba_id templates carry, and what GET /v1/templates?waba_id= and POST /v1/templates/{id}/copy take. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | required | Meta's id for the WhatsApp Business Account this number is on, as Meta's Graph API and WhatsApp Manager show it. Cosend's id for it is waba_id. |
| display_phone_number | string | required | |
| phone_e164 | string | required | |
| verified_name | string | null | required | |
| mode | "coexistence" | "cloud_api" | required | |
| status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| status_reason | string | null | required | |
| status_message | string | null | required | |
| token_verification | "verified" | "stale" | "never" | "invalid" | required | |
| throughput_mps | integer | required | |
| quality_rating | string | null | required | |
| quality_rating_previous | string | null | required | |
| quality_changed_at | string (date-time) | null | required | |
| messaging_limit | string | null | required | |
| meta_business_id | string | null | required | |
| last_app_activity_at | string (date-time) | null | required | |
| delivery_mode | "stored" | "direct" | required | |
| import_contacts | boolean | required | Whether Cosend imports contacts from the phone's address book, as chosen when the number was connected. Read-only. |
| override_callback_url | string | null | required | Your endpoint for Meta’s webhooks for this number. On a direct number Cosend sets it as the number’s webhook override at Meta, and Meta sends the number’s messages, echoes, calls and history to it itself; Meta checks it first with webhook.verify_token, and keeps template and account webhooks on Cosend. On a stored number Cosend receives and keeps every change and relays each one to it in Meta’s envelope, signed X-Cosend-Signature-256: sha256=<HMAC-SHA256 of the body with webhook.relay_secret>, retried with back-off until a 2xx. null clears it. |
| webhook | object | required | |
| webhook.meta | object | null | required | |
| webhook.meta.phone_number | string | null | required | |
| webhook.meta.whatsapp_business_account | string | null | required | |
| webhook.meta.application | string | null | required | |
| webhook.meta.read_at | string (date-time) | required | |
| webhook.verify_token | string | null | required | |
| webhook.relay_secret | string | null | required | |
| webhook.relay | object | required | |
| webhook.relay.last_delivered_at | string (date-time) | null | required | |
| webhook.relay.last_failure | object | null | required | |
| is_test | boolean | required | |
| connected_at | string (date-time) | null | required | |
| registered_at | string (date-time) | null | required | |
| register_deadline_at | string (date-time) | null | required | |
| register_blocked_until | string (date-time) | null | required | |
| display_name_status | string | null | required | |
| display_name | object | required | |
| display_name.current | string | null | required | |
| display_name.requested | string | null | required | |
| display_name.status | "approved" | "not_reviewed" | "pending_review" | "declined" | "not_set" | required | |
| display_name.applied | boolean | null | required | |
| display_name.reason | string | null | required | |
| display_name.updated_at | string (date-time) | null | required | |
| display_name.meta_status | string | null | required | |
| display_name.meta_status_since | string (date-time) | null | required | |
| display_name.meta_review | string | null | required | |
| display_name.meta_review_since | string (date-time) | null | required | |
| display_name.read_at | string (date-time) | null | required | |
| display_name.decision | object | null | required | |
| display_name.decision.value | string | required | |
| display_name.decision.name | string | null | required | |
| display_name.decision.reason_code | string | null | required | |
| display_name.decision.reason | string | null | required | |
| display_name.decision.at | string (date-time) | required | |
| display_name.refusal | object | null | required | |
| display_name.refusal.message | string | null | required | |
| display_name.refusal.at | string (date-time) | required | |
| display_name.applied_at | string (date-time) | null | required | |
| display_name.apply_by | string (date-time) | null | required | |
| display_name.submitted | object | null | required | |
| display_name.submitted.name | string | required | |
| display_name.submitted.at | string (date-time) | required | |
| display_name.submitted.by | "owner" | "cosend" | required | |
| display_name.submitted.refusal | string | null | required | |
| display_name.submissions_in_30_days | integer | required | |
| display_name.next | object | required | |
| display_name.next.action | "none" | "applying" | "waiting" | "asking" | "verify_business" | "submit_name" | "submit_different_name" | "submit_by_hand" | … | required | |
| display_name.next.owner | boolean | required | |
| display_name.next.sentence | string | null | required | |
| display_name.next.until | string (date-time) | null | required | |
| templates_synced_at | string (date-time) | null | required | |
| health | object | null | required | |
| health.checked_at | string (date-time) | required | |
| health.can_send_message | string | null | required | |
| health.read_at | string (date-time) | null | required | |
| health.entities | object[] | required | |
| health.entities[].entity_type | string | null | required | |
| health.entities[].id | string | null | required | |
| health.entities[].can_send_message | string | null | required | |
| health.entities[].can_receive_call_sip | string | null | required | |
| health.entities[].additional_info | string[] | required | |
| health.entities[].errors | object[] | required | |
| health.token | object | null | required | |
| health.token.status | "valid" | "invalid" | "missing_permissions" | "unreachable" | required | |
| health.token.missing_scopes | string[] | required | |
| health.token.expires_at | string (date-time) | null | required | |
| health.token.reason | string | null | required | |
| health.token.action | string | null | required | |
| health.subscription | object | null | required | |
| health.subscription.status | "subscribed" | "not_subscribed" | "unreachable" | required | |
| health.subscription.reason | string | null | required | |
| health.subscription.action | string | null | required | |
| health.can_send | object | null | required | |
| health.can_send.status | "available" | "limited" | "blocked" | "unreachable" | required | |
| health.can_send.reason | string | null | required | |
| health.can_send.action | string | null | required | |
| sending | object | required | |
| sending.status | "can_send" | "limited" | "cannot_send" | "unknown" | required | |
| sending.checked_at | string (date-time) | null | required | |
| sending.problems | object[] | required | |
| sending.problems[].code | string | required | |
| sending.problems[].entity | "number" | "whatsapp_account" | "business" | "app" | "token" | "subscription" | required | |
| sending.problems[].severity | "blocks" | "limits" | "info" | required | |
| sending.problems[].meta | object | required | |
| sending.problems[].title | string | null | required | |
| sending.problems[].sentence | string | null | required | |
| sending.problems[].limit | string | null | required | |
| sending.problems[].causes | object[] | null | required | |
| sending.problems[].fix | object | required | |
| business_verification | object | required | |
| business_verification.status | string | null | required | |
| business_verification.since | string (date-time) | null | required | |
| business_verification.checked_at | string (date-time) | null | required | |
| business_verification.business_id | string | null | required | |
| business_verification.business_name | string | null | required | |
| business_verification.messaging_limit | string | null | required | |
| business_verification.messaging_limit_read_at | string (date-time) | null | required | |
| business_verification.meta | object | null | required | |
| business_verification.meta.description | string | null | required | |
| business_verification.meta.solution | string | null | required | |
| business_verification.alerts | object[] | required | |
| business_verification.alerts[].type | string | required | |
| business_verification.alerts[].severity | string | null | required | |
| business_verification.alerts[].status | string | null | required | |
| business_verification.alerts[].description | string | null | required | |
| business_verification.alerts[].entity_type | string | null | required | |
| business_verification.alerts[].entity_id | string | null | required | |
| business_verification.alerts[].received_at | string (date-time) | required | |
| business_verification.delay | string | null | required | |
| onboarding | object | null | required | |
| onboarding.status | "pending" | "syncing" | "complete" | "expired" | "failed" | "offboarded" | required | |
| onboarding.history_state | "NOT_REQUESTED" | "REQUESTED" | "STREAMING" | "COMPLETE" | "DECLINED" | "MISSED_WINDOW" | "FAILED" | "SKIPPED" | required | |
| onboarding.progress_pct | integer | required | |
| onboarding.messages_synced | integer | required | |
| onboarding.contacts_synced | integer | required | |
| onboarding.sync_deadline_at | string (date-time) | required |
Response 202
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "connect_attempt" | required | |
| status | "connecting" | "connected" | "needs_attention" | required | |
| step | "grant" | "account" | "numbers" | "persist" | "subscribe" | "register" | "sync" | "done" | required | |
| connection_id | string | null | required | |
| waba_id | string | null | required | The WhatsApp Business Account, as Cosend's wab_ id (the waba_id its connection carries), from the step that saves the number on; null before. Meta's id for it is meta_waba_id. |
| meta_waba_id | string | null | required | Meta's id for the WhatsApp Business Account, as Embedded Signup and Meta's Graph API give it; null until Meta has named it. Cosend's id for it is waba_id. |
| phone_number_id | string | null | required | |
| error | object | null | required | |
| error.code | string | required | upstream_unavailable while retrying. When stopped, one of permission_denied, number_in_use, no_number, display_name_rejected, payment_missing or unknown for Meta's answer, or Cosend's own code: connection_exists_elsewhere (another workspace has the number), plan_does_not_include, two_step_pin_required, number_move_refused, rate_limited, or invalid_request with candidates. |
| error.meta_code | integer | null | required | |
| error.message | string | required | |
| error.candidates | object[] | null | required | |
| error.candidates[].phone_number_id | string | required | |
| error.candidates[].display_phone_number | string | null | required | |
| error.candidates[].verified_name | string | null | required | |
| error.candidates[].platform_type | string | null | required | |
| error.candidates[].is_on_biz_app | boolean | null | required | |
| error.customer_step | "turn_off_two_step_verification" | "add_payment_method" | null | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
Errors connection_exists_elsewhere invalid_request number_move_refused plan_does_not_include rate_limited two_step_pin_required upstream_unavailable
/v1/connector-accounts
GET /v1/connector-accounts
The org’s connected accounts, filterable by connector, for re-checking the connected state
Scopes connectors:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connector | query | string | optional | |
| status | query | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | optional | |
| connection_id | query | string | optional | Only the accounts used on this WhatsApp number. Without it, every account of the workspace. |
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].connector | string | required | |
| data[].name | string | required | |
| data[].connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| data[].status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| data[].external_label | string | null | required | |
| data[].external_account_id | string | null | required | |
| data[].granted_scopes | string[] | required | |
| data[].config | object | required | |
| data[].credential_fingerprint | string | null | required | |
| data[].last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| data[].last_used_at | string | null | required | |
| data[].last_verified_at | string | null | required | |
| data[].created_at | string | required | |
| data[].updated_at | string | required | |
| data[].identity | object | null | required | |
| data[].identity.id | string | required | |
| data[].identity.label | string | required | |
| data[].triggers | object[] | optional | |
| data[].triggers[].id | string | required | |
| data[].triggers[].name | string | required | |
| data[].triggers[].trigger | string | required | |
| data[].triggers[].event | string | required | |
| data[].triggers[].mode | string | required | |
| data[].triggers[].automation_id | string | null | required | |
| data[].triggers[].status | string | required | |
| data[].triggers[].last_error | string | null | required | |
| data[].triggers[].last_event_at | string | null | required | |
| data[].triggers[].ingress_url | string | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
GET /v1/connector-accounts/{id}
One connected account. Never its credential
Scopes connectors:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| connector | string | required | |
| name | string | required | |
| connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| external_account_id | string | null | required | |
| granted_scopes | string[] | required | |
| config | object | required | |
| credential_fingerprint | string | null | required | |
| last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| last_used_at | string | null | required | |
| last_verified_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| identity | object | null | required | |
| identity.id | string | required | |
| identity.label | string | required | |
| triggers | object[] | optional | |
| triggers[].id | string | required | |
| triggers[].name | string | required | |
| triggers[].trigger | string | required | |
| triggers[].event | string | required | |
| triggers[].mode | string | required | |
| triggers[].automation_id | string | null | required | |
| triggers[].status | string | required | |
| triggers[].last_error | string | null | required | |
| triggers[].last_event_at | string | null | required | |
| triggers[].ingress_url | string | null | required |
PATCH /v1/connector-accounts/{id}
Rename, or change non-secret config. There is no field here that takes a credential
Scopes connectors:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | |
| config | object | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| connector | string | required | |
| name | string | required | |
| connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| external_account_id | string | null | required | |
| granted_scopes | string[] | required | |
| config | object | required | |
| credential_fingerprint | string | null | required | |
| last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| last_used_at | string | null | required | |
| last_verified_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| identity | object | null | required | |
| identity.id | string | required | |
| identity.label | string | required | |
| triggers | object[] | optional | |
| triggers[].id | string | required | |
| triggers[].name | string | required | |
| triggers[].trigger | string | required | |
| triggers[].event | string | required | |
| triggers[].mode | string | required | |
| triggers[].automation_id | string | null | required | |
| triggers[].status | string | required | |
| triggers[].last_error | string | null | required | |
| triggers[].last_event_at | string | null | required | |
| triggers[].ingress_url | string | null | required |
DELETE /v1/connector-accounts/{id}
Revoke at the provider, then delete the secret
Scopes connectors:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| deleted | true | required | |
| provider_revoked | boolean | required | |
| identity_kept | boolean | required |
GET /v1/connector-accounts/{id}/appointments-preview
A calendar account’s upcoming events and the customer number found in each, or none
Scopes connectors:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| days | query | integer | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| days | integer | required | |
| with_phone | integer | required | |
| without_phone | integer | required | |
| events | object[] | required | |
| events[].external_id | string | required | |
| events[].title | string | null | required | |
| events[].starts_at | string | required | |
| events[].phone | string | null | required |
Errors connector_needs_reauth preview_unsupported rate_limited upstream_error
PATCH /v1/connector-accounts/{id}/credential
Set or replace the key of an account connected with one, keeping its id, name, config and number. The key is never returned
Scopes connectors:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| api_key | string | optional | |
| username | string | optional | |
| password | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| connector | string | required | |
| name | string | required | |
| connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| external_account_id | string | null | required | |
| granted_scopes | string[] | required | |
| config | object | required | |
| credential_fingerprint | string | null | required | |
| last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| last_used_at | string | null | required | |
| last_verified_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| identity | object | null | required | |
| identity.id | string | required | |
| identity.label | string | required | |
| triggers | object[] | optional | |
| triggers[].id | string | required | |
| triggers[].name | string | required | |
| triggers[].trigger | string | required | |
| triggers[].event | string | required | |
| triggers[].mode | string | required | |
| triggers[].automation_id | string | null | required | |
| triggers[].status | string | required | |
| triggers[].last_error | string | null | required | |
| triggers[].last_event_at | string | null | required | |
| triggers[].ingress_url | string | null | required |
DELETE /v1/connector-accounts/{id}/credential
Remove the key of an account whose connector works without one (http); its calls then carry no credential
Scopes connectors:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| connector | string | required | |
| name | string | required | |
| connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| external_account_id | string | null | required | |
| granted_scopes | string[] | required | |
| config | object | required | |
| credential_fingerprint | string | null | required | |
| last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| last_used_at | string | null | required | |
| last_verified_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| identity | object | null | required | |
| identity.id | string | required | |
| identity.label | string | required | |
| triggers | object[] | optional | |
| triggers[].id | string | required | |
| triggers[].name | string | required | |
| triggers[].trigger | string | required | |
| triggers[].event | string | required | |
| triggers[].mode | string | required | |
| triggers[].automation_id | string | null | required | |
| triggers[].status | string | required | |
| triggers[].last_error | string | null | required | |
| triggers[].last_event_at | string | null | required | |
| triggers[].ingress_url | string | null | required |
GET /v1/connector-accounts/{id}/forms
A form account’s forms, with the id an automation’s start takes as its form_id
Scopes connectors:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].object | "form" | required | |
| data[].id | string | required | The form’s id at the form tool: what form_id takes. |
| data[].name | string | required | The form’s name, as the form tool shows it. |
| has_more | boolean | required | The account has more forms than the form tool’s first page, which is what this lists (Jotform 100, Typeform 200). |
POST /v1/connector-accounts/{id}/numbers
Use this integration account on another WhatsApp number of the workspace too: the same account and sign-in, never a copy. Owner or admin. Already on that number answers the account unchanged
Scopes connectors:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | The WhatsApp number of this workspace to use the account on too. Already on it answers 200 with the account. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| connector | string | required | |
| name | string | required | |
| connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| external_account_id | string | null | required | |
| granted_scopes | string[] | required | |
| config | object | required | |
| credential_fingerprint | string | null | required | |
| last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| last_used_at | string | null | required | |
| last_verified_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| identity | object | null | required | |
| identity.id | string | required | |
| identity.label | string | required | |
| triggers | object[] | optional | |
| triggers[].id | string | required | |
| triggers[].name | string | required | |
| triggers[].trigger | string | required | |
| triggers[].event | string | required | |
| triggers[].mode | string | required | |
| triggers[].automation_id | string | null | required | |
| triggers[].status | string | required | |
| triggers[].last_error | string | null | required | |
| triggers[].last_event_at | string | null | required | |
| triggers[].ingress_url | string | null | required |
DELETE /v1/connector-accounts/{id}/numbers/{connection_id}
Stop using this integration account on one WhatsApp number. Owner or admin. Refused on its only number (disconnect it instead) and while an automation switched on there still needs it
Scopes connectors:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| connector | string | required | |
| name | string | required | |
| connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| external_account_id | string | null | required | |
| granted_scopes | string[] | required | |
| config | object | required | |
| credential_fingerprint | string | null | required | |
| last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| last_used_at | string | null | required | |
| last_verified_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| identity | object | null | required | |
| identity.id | string | required | |
| identity.label | string | required | |
| triggers | object[] | optional | |
| triggers[].id | string | required | |
| triggers[].name | string | required | |
| triggers[].trigger | string | required | |
| triggers[].event | string | required | |
| triggers[].mode | string | required | |
| triggers[].automation_id | string | null | required | |
| triggers[].status | string | required | |
| triggers[].last_error | string | null | required | |
| triggers[].last_event_at | string | null | required | |
| triggers[].ingress_url | string | null | required |
POST /v1/connector-accounts/{id}/picker
What the browser opens Google’s file picker with for this Google account: a short-lived drive.file access token, the app id and the API key
Scopes connectors:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| access_token | string | required | |
| expires_at | string | required | |
| app_id | string | required | |
| api_key | string | required |
GET /v1/connector-accounts/{id}/sheets
A Google account’s spreadsheets, 100 a page at most, or one spreadsheet’s tabs with ?spreadsheet_id=
Scopes connectors:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| spreadsheet_id | query | string | optional | |
| limit | query | integer | optional | |
| after | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].object | "spreadsheet" | required | |
| data[].id | string | required | |
| data[].name | string | required | |
| data[].tabs | object[] | optional | |
| data[].tabs[].id | integer | required | |
| data[].tabs[].title | string | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/connector-accounts/{id}/sheets
Make a spreadsheet in this Google account: its name, its tab, the header columns and an example row
Scopes connectors:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | required | The spreadsheet’s name, in the owner’s words. |
| tab | string | required | The one tab’s name. |
| columns | object[] | required | The header row, first column first: 26 at most, A to Z. |
| columns[].name | string | required | The column’s header, as the first row shows it and as Find a row matches it (any case). |
| columns[].type | "text" | "number" | "date" | optional | What the column holds. text, the default: the example row’s cell is formatted as text, so what the owner types over it stays exactly as typed (a phone number, a code). number or date: that cell is not formatted as text, so what the owner types over it is read the way Google Sheets reads anything typed. The example value itself is written as text either way. With no example row it changes nothing in the sheet. |
| example_row | string[] | optional | One row under the header for the owner to type over, as wide as columns. Absent: the sheet has the header row only. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "spreadsheet" | required | |
| id | string | required | |
| name | string | required | |
| tabs | object[] | required | |
| tabs[].id | integer | required | |
| tabs[].title | string | required | |
| url | string | required | |
| columns | object[] | required | |
| columns[].name | string | required | |
| columns[].type | "text" | "number" | "date" | required |
POST /v1/connector-accounts/{id}/test
One safe read against the provider to verify the credential
Scopes connectors:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| ok | boolean | required | |
| status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| external_label | string | null | required | |
| error | string | null | required | |
| message | string | null | required | |
| checked_at | string | required |
POST /v1/connector-accounts/connect
Start a connection: an authorize URL for OAuth, or an immediate connect for a key
Scopes connectors:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connector | string | required | |
| name | string | optional | |
| api_key | string | optional | |
| username | string | optional | |
| password | string | optional | |
| config | object | optional | |
| connection_id | string | null | optional | The WhatsApp number this integration is for. Left out in a workspace with one number, it is that number; with several, the account is on no number until POST /v1/connector-accounts/{id}/numbers adds one. Connecting an account the workspace already has (the same shop, the same sign-in) adds this number to it and keeps its other numbers. |
| identity_id | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| status | "authorize_required" | required | |
| connector | string | required | |
| authorize_url | string | required | |
| expires_in_ms | number | required | |
| account | object | required | |
| account.id | string | required | |
| account.connector | string | required | |
| account.name | string | required | |
| account.connection_ids | string[] | required | The WhatsApp numbers this account is used on, sorted. Only these numbers' automations hear its events and may call it; each number's automations stay its own. One account, one sign-in: every number here uses the same credential. Empty when it is on no number: it is then refused wherever it would be used until POST /v1/connector-accounts/{id}/numbers adds one. |
| account.status | "active" | "needs_reauth" | "error" | "disabled" | "revoked" | required | |
| account.external_label | string | null | required | |
| account.external_account_id | string | null | required | |
| account.granted_scopes | string[] | required | |
| account.config | object | required | |
| account.credential_fingerprint | string | null | required | |
| account.last_error | string | null | required | Why the account last failed, as a sentence to show as it is: the app's own words when it gave some, otherwise Cosend's. Never a bare code; a Test's error carries the code. null when there is none. |
| account.last_used_at | string | null | required | |
| account.last_verified_at | string | null | required | |
| account.created_at | string | required | |
| account.updated_at | string | required | |
| account.identity | object | null | required | |
| account.identity.id | string | required | |
| account.identity.label | string | required | |
| account.triggers | object[] | optional | |
| account.triggers[].id | string | required | |
| account.triggers[].name | string | required | |
| account.triggers[].trigger | string | required | |
| account.triggers[].event | string | required | |
| account.triggers[].mode | string | required | |
| account.triggers[].automation_id | string | null | required | |
| account.triggers[].status | string | required | |
| account.triggers[].last_error | string | null | required | |
| account.triggers[].last_event_at | string | null | required | |
| account.triggers[].ingress_url | string | null | required |
POST /v1/connector-accounts/grants/{id}/claim
Save a connection the OAuth callback holds: only for the person who started it, signed in to the dashboard in its workspace
Scopes connectors:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| connector | string | required | |
| account_id | string | required | The connected account, stored by this claim. |
| identity | object | null | required | The Google login the account is filed under: the one the person signed in with. Null for every other connector. |
| identity.id | string | required | |
| identity.label | string | required |
/v1/connector-identities
GET /v1/connector-identities
The org’s connected Google logins and the integrations each one serves, filterable by number
Scopes connectors:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].family | "google" | required | |
| data[].label | string | required | |
| data[].status | "active" | "needs_reauth" | "revoked" | required | |
| data[].granted_scopes | string[] | required | |
| data[].products | string[] | required | |
| data[].created_at | string | required |
/v1/connectors
GET /v1/connectors
What can be connected, and which of them this org already has
Scopes connectors:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].connector | string | required | |
| data[].display_name | string | required | |
| data[].category | string | required | |
| data[].auth_kind | "oauth2" | "api_key" | "basic" | "none" | required | |
| data[].scopes | string[] | required | |
| data[].credential_label | string | null | required | |
| data[].credential_help_url | string | null | required | |
| data[].operations | object[] | required | |
| data[].operations[].key | string | required | |
| data[].operations[].title | string | required | |
| data[].operations[].side_effect | "none" | "internal_write" | "external_send" | required | |
| data[].operations[].test_reads_for_real | boolean | required | |
| data[].operations[].input_schema | object | required | |
| data[].status | string | required | |
| data[].min_plan | string | null | required | |
| data[].docs_url | string | null | required | |
| data[].connected_account_ids | string[] | required |
/v1/contact-fields
GET /v1/contact-fields
The workspace's contact fields, each with how many contacts hold a value for it
Scopes contacts:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "contact_field" | required | |
| data[].key | string | required | The key in contacts.attributes. Fixed once made. |
| data[].label | string | required | |
| data[].type | "text" | "number" | "date" | "boolean" | "choice" | required | |
| data[].choices | string[] | required | |
| data[].required | boolean | required | A PATCH /v1/contacts/{id} that sets attributes must give this field a value. |
| data[].contacts_with_value | integer | required | |
| data[].created_at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/contact-fields
Add a contact field: a key of contacts.attributes, with its label and type
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| key | string | required | |
| label | string | required | |
| type | "text" | "number" | "date" | "boolean" | "choice" | required | |
| choices | string[] | optional | A choice field's options, 1 to 100 of them, each once; none otherwise. |
| required | boolean | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_field" | required | |
| key | string | required | The key in contacts.attributes. Fixed once made. |
| label | string | required | |
| type | "text" | "number" | "date" | "boolean" | "choice" | required | |
| choices | string[] | required | |
| required | boolean | required | A PATCH /v1/contacts/{id} that sets attributes must give this field a value. |
| contacts_with_value | integer | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required |
PATCH /v1/contact-fields/{id}
Change a contact field's label, type, choices or whether it is required; the key stays
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | optional | |
| type | "text" | "number" | "date" | "boolean" | "choice" | optional | |
| choices | string[] | optional | A choice field's options, 1 to 100 of them, each once; none otherwise. |
| required | boolean | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_field" | required | |
| key | string | required | The key in contacts.attributes. Fixed once made. |
| label | string | required | |
| type | "text" | "number" | "date" | "boolean" | "choice" | required | |
| choices | string[] | required | |
| required | boolean | required | A PATCH /v1/contacts/{id} that sets attributes must give this field a value. |
| contacts_with_value | integer | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required |
DELETE /v1/contact-fields/{id}
Delete a contact field and every contact's value for it
Scopes contacts:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_field" | required | |
| deleted | true | required | |
| contacts_updated | integer | required |
/v1/contact-imports
GET /v1/contact-imports
The workspace's contact imports
Scopes contacts:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "contact_import" | required | |
| data[].state | "draft" | "queued" | "running" | "completed" | "failed" | required | |
| data[].connection_id | string | required | |
| data[].file_name | string | null | required | |
| data[].row_count | integer | required | |
| data[].columns | string[] | required | |
| data[].sample_rows | string[][] | required | |
| data[].mapping | object | required | |
| data[].consent | object | null | required | |
| data[].consent.categories | ("marketing" | "utility" | "authentication")[] | required | |
| data[].consent.sentence_shown | string | null | required | |
| data[].consent.attested_by | string | null | required | |
| data[].progress | object | required | |
| data[].progress.processed | integer | required | |
| data[].progress.total | integer | required | |
| data[].result | object | null | required | |
| data[].result.imported | integer | required | |
| data[].result.failed | object[] | required | |
| data[].result.errors | integer | required | |
| data[].created_at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required | |
| data[].finished_at | string (date-time) | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/contact-imports
Upload a CSV of contacts for one number (base64, or a med_ id); answers its columns, first rows and row count
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| file_name | string | optional | |
| data_base64 | string | optional | The CSV file, base64-encoded. |
| media_id | string | optional | A med_ id from POST /v1/media (text/plain or text/csv), instead of data_base64. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_import" | required | |
| state | "draft" | "queued" | "running" | "completed" | "failed" | required | |
| connection_id | string | required | |
| file_name | string | null | required | |
| row_count | integer | required | |
| columns | string[] | required | |
| sample_rows | string[][] | required | |
| mapping | object | required | |
| consent | object | null | required | |
| consent.categories | ("marketing" | "utility" | "authentication")[] | required | |
| consent.sentence_shown | string | null | required | |
| consent.attested_by | string | null | required | |
| progress | object | required | |
| progress.processed | integer | required | |
| progress.total | integer | required | |
| result | object | null | required | |
| result.imported | integer | required | |
| result.failed | object[] | required | |
| result.failed[].row | integer | required | |
| result.failed[].reason | string | required | |
| result.errors | integer | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
GET /v1/contact-imports/{id}
One contact import: its state, progress and result
Scopes contacts:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_import" | required | |
| state | "draft" | "queued" | "running" | "completed" | "failed" | required | |
| connection_id | string | required | |
| file_name | string | null | required | |
| row_count | integer | required | |
| columns | string[] | required | |
| sample_rows | string[][] | required | |
| mapping | object | required | |
| consent | object | null | required | |
| consent.categories | ("marketing" | "utility" | "authentication")[] | required | |
| consent.sentence_shown | string | null | required | |
| consent.attested_by | string | null | required | |
| progress | object | required | |
| progress.processed | integer | required | |
| progress.total | integer | required | |
| result | object | null | required | |
| result.imported | integer | required | |
| result.failed | object[] | required | |
| result.failed[].row | integer | required | |
| result.failed[].reason | string | required | |
| result.errors | integer | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
PATCH /v1/contact-imports/{id}
Set an import's column mapping, its number, and the consent everyone in the file gave
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| mapping | object | optional | |
| connection_id | string | optional | |
| consent | object | null | optional | |
| consent.categories | ("marketing" | "utility" | "authentication")[] | required | |
| consent.sentence_shown | string | optional | |
| consent.attested_by | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_import" | required | |
| state | "draft" | "queued" | "running" | "completed" | "failed" | required | |
| connection_id | string | required | |
| file_name | string | null | required | |
| row_count | integer | required | |
| columns | string[] | required | |
| sample_rows | string[][] | required | |
| mapping | object | required | |
| consent | object | null | required | |
| consent.categories | ("marketing" | "utility" | "authentication")[] | required | |
| consent.sentence_shown | string | null | required | |
| consent.attested_by | string | null | required | |
| progress | object | required | |
| progress.processed | integer | required | |
| progress.total | integer | required | |
| result | object | null | required | |
| result.imported | integer | required | |
| result.failed | object[] | required | |
| result.failed[].row | integer | required | |
| result.failed[].reason | string | required | |
| result.errors | integer | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
POST /v1/contact-imports/{id}/preview
What running the import would do, over the whole file: new, updated, invalid, duplicates, already opted out, consent recorded
Scopes contacts:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| new | integer | required | |
| updated | integer | required | |
| invalid | integer | required | |
| duplicates | integer | required | |
| already_opted_out | integer | required | |
| consent_recorded | integer | required |
POST /v1/contact-imports/{id}/run
Run the import in the background; GET it to follow its progress and result
Scopes contacts:write
Response 202
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact_import" | required | |
| state | "draft" | "queued" | "running" | "completed" | "failed" | required | |
| connection_id | string | required | |
| file_name | string | null | required | |
| row_count | integer | required | |
| columns | string[] | required | |
| sample_rows | string[][] | required | |
| mapping | object | required | |
| consent | object | null | required | |
| consent.categories | ("marketing" | "utility" | "authentication")[] | required | |
| consent.sentence_shown | string | null | required | |
| consent.attested_by | string | null | required | |
| progress | object | required | |
| progress.processed | integer | required | |
| progress.total | integer | required | |
| result | object | null | required | |
| result.imported | integer | required | |
| result.failed | object[] | required | |
| result.failed[].row | integer | required | |
| result.failed[].reason | string | required | |
| result.errors | integer | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required | |
| finished_at | string (date-time) | null | required |
/v1/contacts
GET /v1/contacts
List contacts; q searches display_name, phone digits and username (page size 20 under q)
Scopes contacts:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| q | query | string | optional | |
| tag | query | string | string[] | optional | |
| connection_id | query | string | optional | |
| order | query | "recent" | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "contact" | required | |
| data[].connection_id | string | required | |
| data[].display_name | string | required | |
| data[].phone_e164 | string | null | required | |
| data[].wa_id | string | null | required | |
| data[].bsuid | string | null | required | |
| data[].username | string | null | required | |
| data[].name | string | null | required | |
| data[].profile_name | string | null | required | |
| data[].attributes | object | required | |
| data[].tags | string[] | required | |
| data[].locale | string | null | required | |
| data[].timezone | string | null | required | |
| data[].is_blocked | boolean | required | |
| data[].removed_in_app_at | string (date-time) | null | required | |
| data[].first_seen_at | string (date-time) | required | |
| data[].last_seen_at | string (date-time) | null | required | |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/contacts
Create a contact. It upserts, so an existing one returns 200
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| phone_e164 | string | null | optional | |
| bsuid | string | null | optional | |
| profile_name | string | null | optional | |
| attributes | object | optional | Free-form values, replaced whole on PATCH. A key registered in /v1/contact-fields must hold a value of its type (text: a string; number: a number; date: YYYY-MM-DD; boolean: true or false; choice: one of its choices) or null; a PATCH that sets attributes must give every required field a value. A key that is not registered is kept as it comes. |
| tags | string[] | optional | |
| locale | string | null | optional | |
| timezone | string | null | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
GET /v1/contacts/{id}
One contact, by id, E.164 or BSUID
Scopes contacts:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
PATCH /v1/contacts/{id}
Update attributes, tags, names and timezone
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | null | optional | |
| profile_name | string | null | optional | |
| attributes | object | optional | Free-form values, replaced whole on PATCH. A key registered in /v1/contact-fields must hold a value of its type (text: a string; number: a number; date: YYYY-MM-DD; boolean: true or false; choice: one of its choices) or null; a PATCH that sets attributes must give every required field a value. A key that is not registered is kept as it comes. |
| tags | string[] | optional | |
| locale | string | null | optional | |
| timezone | string | null | optional | |
| is_blocked | boolean | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
DELETE /v1/contacts/{id}
Soft delete; ?purge=true hard-deletes for GDPR
Scopes contacts:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| purge | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| deleted | true | required | |
| purge | object | optional | |
| purge.erased | object | required | |
| purge.not_erased | string[] | required |
GET /v1/contacts/{id}/consent
Consent state per category, with the evidence each was recorded with
Scopes contacts:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | optional | Accepted and ignored: a contact belongs to one number, so its consent is that number's. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "consent" | required | |
| contact_id | string | required | |
| contact | object | required | |
| contact.id | string | required | |
| contact.bsuid | string | null | required | |
| contact.phone | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | required | |
| categories | object | required | |
| meta_marketing_opt_out | boolean | required | |
| evidence | object | required |
POST /v1/contacts/{id}/consent
Record consent as an append-only ledger entry
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| category | "marketing" | "utility" | "authentication" | required | |
| state | "granted" | "withdrawn" | "unknown" | required | |
| source | string | optional | |
| evidence | object | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "consent" | required | |
| contact_id | string | required | |
| contact | object | required | |
| contact.id | string | required | |
| contact.bsuid | string | null | required | |
| contact.phone | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | required | |
| categories | object | required | |
| meta_marketing_opt_out | boolean | required | |
| evidence | object | required |
POST /v1/contacts/{id}/merge
Merge two identities (phone + BSUID)
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
POST /v1/contacts/{id}/tags
Add tags to a contact; a tag it already has is left as it is
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| tags | string[] | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
DELETE /v1/contacts/{id}/tags
Remove tags from a contact; a tag it does not have is ignored
Scopes contacts:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| tags | string[] | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "contact" | required | |
| connection_id | string | required | |
| display_name | string | required | |
| phone_e164 | string | null | required | |
| wa_id | string | null | required | |
| bsuid | string | null | required | |
| username | string | null | required | |
| name | string | null | required | |
| profile_name | string | null | required | |
| attributes | object | required | |
| tags | string[] | required | |
| locale | string | null | required | |
| timezone | string | null | required | |
| is_blocked | boolean | required | |
| removed_in_app_at | string (date-time) | null | required | |
| first_seen_at | string (date-time) | required | |
| last_seen_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
GET /v1/contacts/{id}/threads
This person’s chats on each number the caller may see (the same phone number or BSUID), newest first
Scopes contacts:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].connection_id | string | required | |
| data[].contact_id | string | required | |
| data[].conversation_id | string | required | |
| data[].state | "open" | "pending" | "snoozed" | "resolved" | required | |
| data[].last_message_at | string (date-time) | null | required | |
| data[].window | object | required | |
| data[].window.state | "open" | "expiring" | "closed" | required | |
| data[].window.band | "open" | "closing" | "expiring" | "closed" | required | |
| data[].window.expires_at | string (date-time) | null | required |
GET /v1/contacts/{id}/window
Messaging-window state for a contact
Scopes contacts:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| service_window_open | boolean | required | |
| service_window_expires_at | string (date-time) | null | required | |
| free_entry_point_expires_at | string (date-time) | null | required | |
| can_send_freeform | boolean | required | |
| expires_in_seconds | integer | null | required | |
| state | "open" | "expiring" | "closed" | required | |
| band | "open" | "closing" | "expiring" | "closed" | required | |
| object | "window" | required | |
| contact_id | string | required | |
| conversation_id | string | null | required |
/v1/conversations
GET /v1/conversations
List conversations, filtered; q searches the contact and the newest message (page size 20 under q)
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| q | query | string | optional | |
| state | query | string | optional | |
| control | query | string | optional | |
| assignee_id | query | string | optional | |
| assignee | query | "none" | "me" | optional | |
| connection_id | query | string | string[] | optional | |
| include_direct | query | string | optional | |
| label | query | string | optional | |
| unread | query | string | optional | |
| waiting | query | string | optional | |
| needs_person | query | string | optional | |
| window_state | query | "open" | "closing" | "expiring" | "closed" | ("open" | "closing" | "expiring" | "closed")[] | optional | |
| order | query | "recent" | "urgency" | "waiting_longest" | optional | |
| priority | query | "normal" | "priority" | "high_priority" | optional | |
| expand | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "conversation" | required | |
| data[].connection_id | string | required | |
| data[].contact_id | string | required | |
| data[].state | "open" | "pending" | "snoozed" | "resolved" | required | |
| data[].control | "bot" | "pending_human" | "human" | "app" | required | |
| data[].assignee_id | string | null | required | |
| data[].unread_count | integer | required | |
| data[].message_count | integer | required | |
| data[].last_message_at | string (date-time) | null | required | |
| data[].snoozed_until | string (date-time) | null | required | |
| data[].window | object | required | |
| data[].window.service_window_open | boolean | required | |
| data[].window.service_window_expires_at | string (date-time) | null | required | |
| data[].window.free_entry_point_expires_at | string (date-time) | null | required | |
| data[].window.can_send_freeform | boolean | required | |
| data[].window.expires_in_seconds | integer | null | required | |
| data[].window.state | "open" | "expiring" | "closed" | required | |
| data[].window.band | "open" | "closing" | "expiring" | "closed" | required | |
| data[].lock | object | required | |
| data[].lock.held | boolean | required | |
| data[].lock.held_by | string | null | required | |
| data[].lock.held_by_name | string | null | required | |
| data[].lock.idle | boolean | required | |
| data[].lock.expires_at | string (date-time) | null | required | |
| data[].lock.can_i_send | boolean | required | |
| data[].lock.can_i_take_over | "yes" | "request" | "no" | required | |
| data[].lock.pending_request | object | null | required | |
| data[].waiting | object | null | required | |
| data[].waiting.since | string (date-time) | required | |
| data[].waiting.reason | "unanswered" | "handed_over" | required | |
| data[].waiting.summary | string | null | required | |
| data[].needs_person | object | null | required | |
| data[].needs_person.since | string (date-time) | required | |
| data[].needs_person.reason | string | null | required | |
| data[].needs_person.summary | string | null | required | |
| data[].priority | "normal" | "priority" | "high_priority" | required | |
| data[].resolved_at | string (date-time) | null | required | |
| data[].resolved_by | string | null | required | |
| data[].labels | string[] | required | |
| data[].created_at | string (date-time) | required | |
| data[].contact | object | null | required | |
| data[].contact.id | string | required | |
| data[].contact.object | "contact" | required | |
| data[].contact.connection_id | string | required | |
| data[].contact.display_name | string | required | |
| data[].contact.phone_e164 | string | null | required | |
| data[].contact.wa_id | string | null | required | |
| data[].contact.bsuid | string | null | required | |
| data[].contact.username | string | null | required | |
| data[].contact.name | string | null | required | |
| data[].contact.profile_name | string | null | required | |
| data[].contact.attributes | object | required | |
| data[].contact.tags | string[] | required | |
| data[].contact.locale | string | null | required | |
| data[].contact.timezone | string | null | required | |
| data[].contact.is_blocked | boolean | required | |
| data[].contact.removed_in_app_at | string (date-time) | null | required | |
| data[].contact.first_seen_at | string (date-time) | required | |
| data[].contact.last_seen_at | string (date-time) | null | required | |
| data[].contact.created_at | string (date-time) | required | |
| data[].last_message | object | null | required | |
| data[].last_message.id | string | required | |
| data[].last_message.object | "message" | required | |
| data[].last_message.conversation_id | string | required | |
| data[].last_message.contact_id | string | required | |
| data[].last_message.contact | object | required | |
| data[].last_message.connection_id | string | required | |
| data[].last_message.wamid | string | null | required | |
| data[].last_message.direction | "inbound" | "outbound" | required | |
| data[].last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| data[].last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| data[].last_message.type | string | required | |
| data[].last_message.content | object | required | |
| data[].last_message.location | object | null | required | |
| data[].last_message.contact_cards | object[] | null | required | |
| data[].last_message.label | string | null | required | |
| data[].last_message.forwarded | boolean | required | |
| data[].last_message.frequently_forwarded | boolean | required | |
| data[].last_message.tap | object | null | required | |
| data[].last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| data[].last_message.reply_to | object | null | required | |
| data[].last_message.reactions | object[] | required | |
| data[].last_message.metadata | object | required | |
| data[].last_message.created_at | string (date-time) | required | |
| data[].last_message.wa_timestamp | string (date-time) | required | |
| data[].last_message.is_imported | boolean | required | |
| data[].last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| data[].last_message.media_expires_at | string (date-time) | null | required | |
| data[].last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| data[].last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| data[].last_message.client_token | string | null | required | |
| data[].last_message.author_user_id | string | null | required | |
| data[].last_message.automation_run_id | string | null | required | |
| data[].last_message.error_code | integer | null | required | |
| data[].last_message.error_title | string | null | required | |
| data[].last_message.error_detail | string | null | required | |
| data[].connection | object | null | required | |
| data[].connection.id | string | required | |
| data[].connection.verified_name | string | null | required | |
| data[].connection.display_phone_number | string | required | |
| data[].connection.delivery_mode | "stored" | "direct" | required | |
| data[].last_read_at | string (date-time) | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
GET /v1/conversations/{id}
One conversation, with its window and lock blocks
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| expand | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
PATCH /v1/conversations/{id}
State, assignee, snooze, labels and priority. state: resolved is Done, the same as POST /resolve
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| state | "open" | "pending" | "snoozed" | "resolved" | optional | |
| assignee_id | string | null | optional | |
| snoozed_until | string (date-time) | null | optional | |
| labels | string[] | optional | |
| priority | "normal" | "priority" | "high_priority" | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
POST /v1/conversations/{id}/control
Bot ↔ human handoff
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| control | "pending_human" | "human" | required | |
| reason | string | optional | |
| pause_bot_until | string (date-time) | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
POST /v1/conversations/{id}/draft
Draft a reply in this chat from the number's AI assistant and this chat only. Nothing is sent
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| instruction | string | null | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "conversation_draft" | required | |
| conversation_id | string | required | |
| text | string | null | required | The draft, in the customer’s language. Null when there is none: message says why. |
| sources | object[] | required | What it was written from: the number's AI assistant (assistant, its automation id), or, with none, a neutral persona named from the number's business profile (business_profile); and each knowledge source it read (knowledge, its id). |
| sources[].kind | "assistant" | "business_profile" | "knowledge" | required | |
| sources[].title | string | required | |
| sources[].id | string | null | required | |
| detected_language | string | null | required | The language the customer writes in, as a language tag such as es or pt-BR. Null when it could not be told. |
| flagged | boolean | required | True when OpenAI's moderation check flagged the customer's latest message: no draft, and nothing is handed over. |
| message | string | null | required | Why there is no draft, in words. Null when there is one. |
GET /v1/conversations/{id}/events
The activity feed for one conversation; kind (repeatable) keeps those kinds
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| kind | query | string | string[] | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "conversation_event" | required | |
| data[].conversation_id | string | required | |
| data[].actor_id | string | null | required | |
| data[].kind | string | required | |
| data[].data | object | required | What the line records; its keys depend on kind. A handed_over line carries from, to, reason (as the hand-over gave it, when it gave one) and summary: why the chat was passed to a person, in words: this hand-over's own sentence, e.g. "Passed to a person because the assistant did not have the answer (booking).", or null when it said nothing. Never a code. One an automation's run made also carries run_id and automation_id. |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/conversations/{id}/lock
Acquire the lock. It holds until released, handed over or transferred; no expiry
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| activity | boolean | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
DELETE /v1/conversations/{id}/lock
Release the lock. Fired on leaving the chat and on tab close
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
POST /v1/conversations/{id}/lock/requests
Ask the holder to hand over; silence transfers in 60s
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| reason | string | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "lock_request" | required | |
| conversation_id | string | required | |
| expires_at | string (date-time) | required | |
| held_by | string | required | |
| requested_by | string | required | |
| state | "pending" | "granted" | "declined" | "expired" | "superseded" | required | |
| auto_grants | true | required |
GET /v1/conversations/{id}/notes
Internal notes on a conversation
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "note" | required | |
| data[].conversation_id | string | required | |
| data[].author_id | string | required | |
| data[].body | string | required | |
| data[].mentions | string[] | required | |
| data[].created_at | string (date-time) | required | |
| data[].edited_at | string (date-time) | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/conversations/{id}/notes
Add an internal note, with @mentions
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| body | string | required | |
| mentions | string[] | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "note" | required | |
| conversation_id | string | required | |
| author_id | string | required | |
| body | string | required | |
| mentions | string[] | required | |
| created_at | string (date-time) | required | |
| edited_at | string (date-time) | null | required |
PATCH /v1/conversations/{id}/notes/{noteId}
Edit an internal note. Only its author can
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| body | string | required | |
| mentions | string[] | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "note" | required | |
| conversation_id | string | required | |
| author_id | string | required | |
| body | string | required | |
| mentions | string[] | required | |
| created_at | string (date-time) | required | |
| edited_at | string (date-time) | null | required |
DELETE /v1/conversations/{id}/notes/{noteId}
Delete an internal note. Its author, or an owner or admin, can
Scopes messages:write
Response 204
No content
POST /v1/conversations/{id}/read
Mark the whole thread read (internal; no blue ticks)
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
POST /v1/conversations/{id}/read-receipt
Blue ticks for every unread inbound message in the thread
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| ok | true | required |
POST /v1/conversations/{id}/resolve
Mark Resolved: nobody holds the chat, nothing is sent, and automations treat the next message as a first message
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "conversation" | required | |
| connection_id | string | required | |
| contact_id | string | required | |
| state | "open" | "pending" | "snoozed" | "resolved" | required | |
| control | "bot" | "pending_human" | "human" | "app" | required | |
| assignee_id | string | null | required | |
| unread_count | integer | required | |
| message_count | integer | required | |
| last_message_at | string (date-time) | null | required | |
| snoozed_until | string (date-time) | null | required | |
| window | object | required | |
| window.service_window_open | boolean | required | |
| window.service_window_expires_at | string (date-time) | null | required | |
| window.free_entry_point_expires_at | string (date-time) | null | required | |
| window.can_send_freeform | boolean | required | |
| window.expires_in_seconds | integer | null | required | |
| window.state | "open" | "expiring" | "closed" | required | |
| window.band | "open" | "closing" | "expiring" | "closed" | required | |
| lock | object | required | |
| lock.held | boolean | required | |
| lock.held_by | string | null | required | |
| lock.held_by_name | string | null | required | |
| lock.idle | boolean | required | |
| lock.expires_at | string (date-time) | null | required | |
| lock.can_i_send | boolean | required | |
| lock.can_i_take_over | "yes" | "request" | "no" | required | |
| lock.pending_request | object | null | required | |
| lock.pending_request.id | string | required | |
| lock.pending_request.requested_by | string | required | |
| lock.pending_request.requested_by_name | string | null | required | |
| lock.pending_request.auto_grants_at | string (date-time) | required | |
| waiting | object | null | required | |
| waiting.since | string (date-time) | required | |
| waiting.reason | "unanswered" | "handed_over" | required | |
| waiting.summary | string | null | required | |
| needs_person | object | null | required | |
| needs_person.since | string (date-time) | required | |
| needs_person.reason | string | null | required | |
| needs_person.summary | string | null | required | |
| priority | "normal" | "priority" | "high_priority" | required | |
| resolved_at | string (date-time) | null | required | |
| resolved_by | string | null | required | |
| labels | string[] | required | |
| created_at | string (date-time) | required | |
| contact | object | null | required | |
| contact.id | string | required | |
| contact.object | "contact" | required | |
| contact.connection_id | string | required | |
| contact.display_name | string | required | |
| contact.phone_e164 | string | null | required | |
| contact.wa_id | string | null | required | |
| contact.bsuid | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | null | required | |
| contact.profile_name | string | null | required | |
| contact.attributes | object | required | |
| contact.tags | string[] | required | |
| contact.locale | string | null | required | |
| contact.timezone | string | null | required | |
| contact.is_blocked | boolean | required | |
| contact.removed_in_app_at | string (date-time) | null | required | |
| contact.first_seen_at | string (date-time) | required | |
| contact.last_seen_at | string (date-time) | null | required | |
| contact.created_at | string (date-time) | required | |
| last_message | object | null | required | |
| last_message.id | string | required | |
| last_message.object | "message" | required | |
| last_message.conversation_id | string | required | |
| last_message.contact_id | string | required | |
| last_message.contact | object | required | |
| last_message.contact.id | string | required | |
| last_message.contact.bsuid | string | null | required | |
| last_message.contact.phone | string | null | required | |
| last_message.contact.username | string | null | required | |
| last_message.contact.name | string | required | |
| last_message.connection_id | string | required | |
| last_message.wamid | string | null | required | |
| last_message.direction | "inbound" | "outbound" | required | |
| last_message.source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| last_message.status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| last_message.type | string | required | |
| last_message.content | object | required | |
| last_message.location | object | null | required | |
| last_message.location.latitude | number | required | |
| last_message.location.longitude | number | required | |
| last_message.location.name | string | null | required | |
| last_message.location.address | string | null | required | |
| last_message.location.url | string | null | required | |
| last_message.contact_cards | object[] | null | required | |
| last_message.contact_cards[].name | string | null | required | |
| last_message.contact_cards[].phones | object[] | required | |
| last_message.contact_cards[].emails | object[] | required | |
| last_message.contact_cards[].org | object | null | required | |
| last_message.label | string | null | required | |
| last_message.forwarded | boolean | required | |
| last_message.frequently_forwarded | boolean | required | |
| last_message.tap | object | null | required | |
| last_message.tap.kind | "button_reply" | "list_reply" | "button" | required | |
| last_message.tap.id | string | required | |
| last_message.tap.title | string | required | |
| last_message.tap.description | string | null | required | |
| last_message.template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| last_message.template_text.header | string | null | required | |
| last_message.template_text.body | string | null | required | |
| last_message.template_text.footer | string | null | required | |
| last_message.template_text.buttons | string[] | required | |
| last_message.reply_to | object | null | required | |
| last_message.reply_to.id | string | null | required | |
| last_message.reply_to.wamid | string | required | |
| last_message.reply_to.kind | string | null | required | |
| last_message.reply_to.from | "contact" | "business" | null | required | |
| last_message.reply_to.preview | string | required | |
| last_message.reactions | object[] | required | |
| last_message.reactions[].emoji | string | required | |
| last_message.reactions[].direction | "inbound" | "outbound" | required | |
| last_message.reactions[].message_id | string | required | |
| last_message.reactions[].created_at | string (date-time) | required | |
| last_message.metadata | object | required | |
| last_message.created_at | string (date-time) | required | |
| last_message.wa_timestamp | string (date-time) | required | |
| last_message.is_imported | boolean | required | |
| last_message.media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| last_message.media_expires_at | string (date-time) | null | required | |
| last_message.media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| last_message.media.id | string | null | required | |
| last_message.media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| last_message.media.mime_type | string | null | required | |
| last_message.media.size_bytes | integer | null | required | |
| last_message.media.filename | string | null | required | |
| last_message.media.caption | string | null | required | |
| last_message.media.animated | boolean | null | required | |
| last_message.media.status | "pending" | "ready" | "failed" | required | |
| last_message.media.failure_reason | string | null | required | |
| last_message.transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| last_message.transcript.status | "ready" | "failed" | "skipped" | required | |
| last_message.transcript.text | string | null | required | |
| last_message.transcript.language | string | null | required | |
| last_message.transcript.model | string | null | required | |
| last_message.transcript.failure_reason | string | null | required | |
| last_message.client_token | string | null | required | |
| last_message.author_user_id | string | null | required | |
| last_message.automation_run_id | string | null | required | |
| last_message.error_code | integer | null | required | |
| last_message.error_title | string | null | required | |
| last_message.error_detail | string | null | required | |
| connection | object | null | required | |
| connection.id | string | required | |
| connection.verified_name | string | null | required | |
| connection.display_phone_number | string | required | |
| connection.delivery_mode | "stored" | "direct" | required | |
| last_read_at | string (date-time) | null | required |
POST /v1/conversations/{id}/typing
Show a typing indicator to the contact
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| ok | true | required |
POST /v1/conversations/bulk
One action on up to 100 chats (resolve as Done, assign, label, priority or read), with a result per chat
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| ids | string[] | required | |
| action | "resolve" | "assign" | "label" | "priority" | "read" | required | |
| assignee_id | string | null | optional | |
| labels_add | string[] | optional | |
| labels_remove | string[] | optional | |
| priority | "normal" | "priority" | "high_priority" | optional | |
| read | boolean | optional |
Response 207
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| results | object[] | required | |
| results[].id | string | required | |
| results[].ok | boolean | required | |
| results[].code | string | optional | |
| results[].message | string | optional |
GET /v1/conversations/counts
Counts for the inbox: waiting, mine, unread, all and windows closing, per number and in total, and the approvals pending
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | string[] | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| waiting | integer | required | |
| mine | integer | required | |
| unread | integer | required | |
| all | integer | required | |
| windows_closing | integer | required | |
| object | "conversation_counts" | required | |
| approvals_pending | integer | required | |
| per_connection | object | required | |
| computed_at | string (date-time) | required |
GET /v1/conversations/labels
The labels in use on each number, with how many open chats carry each
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | string[] | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].label | string | required | |
| data[].connection_id | string | required | |
| data[].open_count | integer | required |
/v1/events
GET /v1/events
The organization’s audit trail. Read-only, always
Scopes none; needs a credential
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| action | query | string | optional | |
| actor_id | query | string | optional | |
| target_id | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "event" | required | |
| data[].action | string | required | |
| data[].actor_kind | string | required | |
| data[].actor_id | string | null | required | |
| data[].actor_label | string | null | required | |
| data[].target_kind | string | null | required | |
| data[].target_id | string | null | required | |
| data[].summary | string | null | required | |
| data[].request_id | string | null | required | |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
/v1/exports
POST /v1/exports
Request an export of the organization’s contacts, chats and log history, or of one person’s data
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| from | string (date-time) | optional | |
| to | string (date-time) | optional | |
| tables | ("contacts" | "contact_consents" | "conversations" | "messages" | "runs" | "run_events" | "connector_calls" | "webhook_ingress" | …)[] | optional | |
| contact_id | string | null | optional |
Response 202
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "export" | required | |
| status | "queued" | "running" | "ready" | "failed" | "expired" | required | |
| window_start | string | required | |
| window_end | string | required | |
| tables | ("contacts" | "contact_consents" | "conversations" | "messages" | "runs" | "run_events" | "connector_calls" | "webhook_ingress" | …)[] | required | |
| contact_id | string | null | required | |
| row_count | integer | null | required | |
| byte_count | integer | null | required | |
| download_url | string | null | required | |
| created_at | string | required | |
| expires_at | string | null | required |
GET /v1/exports/{id}
Status of an export, with the download link once ready
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "export" | required | |
| status | "queued" | "running" | "ready" | "failed" | "expired" | required | |
| window_start | string | required | |
| window_end | string | required | |
| tables | ("contacts" | "contact_consents" | "conversations" | "messages" | "runs" | "run_events" | "connector_calls" | "webhook_ingress" | …)[] | required | |
| contact_id | string | null | required | |
| row_count | integer | null | required | |
| byte_count | integer | null | required | |
| download_url | string | null | required | |
| created_at | string | required | |
| expires_at | string | null | required |
/v1/inbox
POST /v1/inbox/session
Claim the caller’s Inbox seat: one window per person; take_over moves it here
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| take_over | boolean | optional | |
| label | string | null | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "inbox_session" | required | |
| user_id | string | required | |
| label | string | null | required | |
| claimed_at | string (date-time) | required | |
| renewed_at | string (date-time) | required |
DELETE /v1/inbox/session/{id}
Give the seat up. Fired on closing the Inbox and at sign-out
Scopes messages:write
Response 204
No content
POST /v1/inbox/session/{id}/heartbeat
Renew the seat; 409 inbox_session_replaced once another window took it
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "inbox_session" | required | |
| user_id | string | required | |
| label | string | null | required | |
| claimed_at | string (date-time) | required | |
| renewed_at | string (date-time) | required |
Errors inbox_session_replaced
/v1/inbox-views
GET /v1/inbox-views
The saved Inbox views: the caller’s own and every one shared with the workspace
Scopes messages:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "inbox_view" | required | |
| data[].name | string | required | |
| data[].filters | object | required | |
| data[].shared | "private" | "workspace" | required | |
| data[].created_by | string | required | |
| data[].created_at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required |
POST /v1/inbox-views
Save an Inbox view: a name and the GET /v1/conversations filters it applies
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | |
| filters | object | required | |
| filters.limit | integer | optional | |
| filters.q | string | optional | |
| filters.state | string | optional | |
| filters.control | string | optional | |
| filters.assignee_id | string | optional | |
| filters.assignee | "none" | "me" | optional | |
| filters.connection_id | string | string[] | optional | |
| filters.include_direct | string | optional | |
| filters.label | string | optional | |
| filters.unread | string | optional | |
| filters.waiting | string | optional | |
| filters.needs_person | string | optional | |
| filters.window_state | "open" | "closing" | "expiring" | "closed" | ("open" | "closing" | "expiring" | "closed")[] | optional | |
| filters.order | "recent" | "urgency" | "waiting_longest" | optional | |
| filters.priority | "normal" | "priority" | "high_priority" | optional | |
| filters.expand | string | optional | |
| shared | "private" | "workspace" | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "inbox_view" | required | |
| name | string | required | |
| filters | object | required | |
| shared | "private" | "workspace" | required | |
| created_by | string | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required |
PATCH /v1/inbox-views/{id}
Rename a view, change its filters or who sees it. A shared view: its creator, an owner or an admin
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | |
| filters | object | optional | |
| filters.limit | integer | optional | |
| filters.q | string | optional | |
| filters.state | string | optional | |
| filters.control | string | optional | |
| filters.assignee_id | string | optional | |
| filters.assignee | "none" | "me" | optional | |
| filters.connection_id | string | string[] | optional | |
| filters.include_direct | string | optional | |
| filters.label | string | optional | |
| filters.unread | string | optional | |
| filters.waiting | string | optional | |
| filters.needs_person | string | optional | |
| filters.window_state | "open" | "closing" | "expiring" | "closed" | ("open" | "closing" | "expiring" | "closed")[] | optional | |
| filters.order | "recent" | "urgency" | "waiting_longest" | optional | |
| filters.priority | "normal" | "priority" | "high_priority" | optional | |
| filters.expand | string | optional | |
| shared | "private" | "workspace" | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "inbox_view" | required | |
| name | string | required | |
| filters | object | required | |
| shared | "private" | "workspace" | required | |
| created_by | string | required | |
| created_at | string (date-time) | required | |
| updated_at | string (date-time) | required |
DELETE /v1/inbox-views/{id}
Delete a view. A shared view: its creator, an owner or an admin
Scopes messages:write
Response 204
No content
/v1/lock-requests
POST /v1/lock-requests/{id}/decline
Holder refuses a takeover request
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "lock_request" | required | |
| conversation_id | string | required | |
| expires_at | string (date-time) | required | |
| held_by | string | required | |
| requested_by | string | required | |
| state | "pending" | "granted" | "declined" | "expired" | "superseded" | required | |
| auto_grants | true | required |
POST /v1/lock-requests/{id}/grant
Holder hands the lock over now, without waiting out the 60s
Scopes messages:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "lock_request" | required | |
| conversation_id | string | required | |
| expires_at | string (date-time) | required | |
| held_by | string | required | |
| requested_by | string | required | |
| state | "pending" | "granted" | "declined" | "expired" | "superseded" | required | |
| auto_grants | true | required |
/v1/mcp
POST /v1/mcp/consent
Approve or refuse an AI app’s sign-in; answers the address to send the browser to
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| request | string | required | The request the dashboard was opened with: /authorize?request=…. |
| decision | "approve" | "deny" | required | |
| scopes | string[] | optional | The permissions the person kept ticked. Omitted: every one the app asked for that this person may give. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| redirect_to | string | required | Send the browser here. It carries the code, or the refusal, back to the AI app. |
POST /v1/mcp/consent/preview
What an AI app asks to do, for the consent screen; dashboard sessions only
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| request | string | required | The request the dashboard was opened with: /authorize?request=…. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| client | object | required | |
| client.name | string | required | What the app calls itself. Not verified. |
| client.host | string | required | Where the app’s details live, or where it will send you back to. Show it beside the name: it is the part a person can check. |
| client.kind | "metadata_document" | "registered" | required | |
| redirect_host | string | required | |
| scopes | object[] | required | |
| scopes[].scope | string | required | |
| scopes[].description | string | required | |
| scopes[].grantable | boolean | required | |
| scopes[].reason | string | null | required | |
| expires_at | string | required |
/v1/me
GET /v1/me
The calling credential, its organization, plan and limits
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| user | object | null | required | |
| user.id | string | required | |
| user.email | string | required | |
| user.name | string | required | |
| organization | object | required | |
| organization.id | string | required | |
| organization.name | string | required | |
| organization.slug | string | required | |
| organization.timezone | string | required | |
| organization.country | string | null | required | |
| organization.erasure_scheduled_for | string (date-time) | null | required | |
| credential | object | required | |
| credential.kind | "connection" | "organization" | "oauth" | "dashboard" | required | |
| credential.id | string | required | |
| credential.name | string | required | |
| credential.is_live | boolean | required | |
| credential.scopes | string[] | required | |
| credential.connection_id | string | null | required | |
| credential.connection_ids | string[] | required | |
| connections | object[] | required | The WhatsApp numbers this credential works with: every number of the workspace, or, for a key made for some numbers, those. A message is sent from one of them. |
| connections[].id | string | required | |
| connections[].display_phone_number | string | required | |
| connections[].verified_name | string | null | required | |
| connections[].mode | "coexistence" | "cloud_api" | required | |
| connections[].status | "pending" | "active" | "needs_reauth" | "payment_blocked" | "restricted" | "disconnected" | "offboarded" | required | |
| subscription | object | required | |
| subscription.plan | string | required | |
| subscription.status | string | required | |
| subscription.overdue | boolean | required | |
| subscription.overdue_since | string (date-time) | null | required | |
| subscription.current_period_end | string (date-time) | required | |
| subscription.free_limits_effective_at | string (date-time) | null | required | |
| subscription.overdue_stage | "grace" | "dropped" | null | required | |
| subscription.free_without_card | boolean | required | True when automations stop at the allowance: the Free plan with no card on file, or an account held to the Free plan’s limits after a failed payment. False when usage beyond the allowance is billed instead. |
| limits | object | required | |
| entitlements | object | required |
PATCH /v1/me
Change your own name
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| string | required | ||
| name | string | required |
DELETE /v1/me/locks
Release every conversation lock the caller holds. Called at sign-out
Scopes messages:write
Response 204
No content
GET /v1/me/preferences
Your preferences in this workspace: appearance, remembered numbers, shortcuts
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| appearance | "system" | "light" | "dark" | required | |
| remembered_numbers | object | required | |
| remembered_numbers.inbox | "all" | string[] | required | |
| remembered_numbers.ai | string | null | required | |
| remembered_numbers.integrations | string | null | required | |
| single_key_shortcuts | boolean | required |
PATCH /v1/me/preferences
Change any of your preferences; the rest stay as they are
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| appearance | "system" | "light" | "dark" | optional | |
| remembered_numbers | object | optional | |
| remembered_numbers.inbox | "all" | string[] | optional | |
| remembered_numbers.ai | string | null | optional | |
| remembered_numbers.integrations | string | null | optional | |
| single_key_shortcuts | boolean | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| appearance | "system" | "light" | "dark" | required | |
| remembered_numbers | object | required | |
| remembered_numbers.inbox | "all" | string[] | required | |
| remembered_numbers.ai | string | null | required | |
| remembered_numbers.integrations | string | null | required | |
| single_key_shortcuts | boolean | required |
POST /v1/me/send-link
Email yourself a dashboard link, to open on a computer. Five an hour
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| sent_to | string | required |
/v1/media
POST /v1/media
Upload a file to Meta and R2 in one call. Every upload is its own file on its number, even with the same bytes; a name that number already has gets “ (2)”, “ (3)” … before the extension, as in Windows
Scopes messages:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| mime_type | string | required | One of the types WhatsApp accepts, which also decides how the file is sent: image (image/jpeg, image/png; 5 MB), video (video/mp4, video/3gpp; 16 MB), audio (audio/aac, audio/amr, audio/mpeg, audio/mp4, audio/ogg; 16 MB), document (application/pdf, text/plain and the Word, Excel and PowerPoint types; 100 MB), sticker (image/webp; 100 KB static, 500 KB animated). Any other type, or a file over its limit, is refused before anything is stored. |
| filename | string | null | optional | |
| data_base64 | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "media" | required | |
| connection_id | string | required | |
| meta_media_id | string | null | required | |
| mime_type | string | required | |
| filename | string | null | required | |
| size_bytes | integer | required | |
| sha256 | string | required | |
| state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| expires_at | string (date-time) | null | required | |
| created_at | string (date-time) | required | |
| url | string | null | required |
GET /v1/media/{id}
Media metadata plus a signed 15-minute URL
Scopes messages:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "media" | required | |
| connection_id | string | required | |
| meta_media_id | string | null | required | |
| mime_type | string | required | |
| filename | string | null | required | |
| size_bytes | integer | required | |
| sha256 | string | required | |
| state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| expires_at | string (date-time) | null | required | |
| created_at | string (date-time) | required | |
| url | string | null | required |
GET /v1/media/{id}/content
Stream the bytes
Scopes messages:read
Response 200
The media object itself. The Content-Type header carries the real type Meta stored it under, and Content-Disposition names the file when one is known. application/octet-stream
/v1/memberships
GET /v1/memberships
Members, pending invites, and what the next seat costs
Scopes members:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "membership" | required | |
| data[].user_id | string | required | |
| data[].email | string | required | |
| data[].display_name | string | null | required | |
| data[].role | "owner" | "admin" | "member" | required | |
| data[].status | "active" | "invited" | "removed" | required | |
| data[].invited_at | string (date-time) | null | required | |
| data[].invited_by | string | null | required | |
| data[].invited_by_name | string | null | required | |
| data[].invited_by_email | string | null | required | |
| data[].invite_expires_at | string (date-time) | null | required | |
| data[].accepted_at | string (date-time) | null | required | |
| data[].invite_expired | boolean | required | |
| seats | object | required | |
| seats.seats_used | integer | required | |
| seats.seats_included | integer | null | required | |
| seats.pending_invites | integer | required | |
| seats.next_invoice_delta_minor | string | required |
PATCH /v1/memberships/{id}
Change a member’s role. Demoting the last owner is 409 sole_owner
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| role | "owner" | "admin" | "member" | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "membership" | required | |
| user_id | string | required | |
| string | required | ||
| display_name | string | null | required | |
| role | "owner" | "admin" | "member" | required | |
| status | "active" | "invited" | "removed" | required | |
| invited_at | string (date-time) | null | required | |
| invited_by | string | null | required | |
| invited_by_name | string | null | required | |
| invited_by_email | string | null | required | |
| invite_expires_at | string (date-time) | null | required | |
| accepted_at | string (date-time) | null | required | |
| invite_expired | boolean | required |
Errors sole_owner
DELETE /v1/memberships/{id}
Remove a member. Access ends immediately; billing at the period boundary
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "membership" | required | |
| status | "removed" | required | |
| billing_effective_at | string (date-time) | required | |
| seats | object | required | |
| seats.seats_used | integer | required | |
| seats.seats_included | integer | null | required | |
| seats.pending_invites | integer | required | |
| seats.next_invoice_delta_minor | string | required |
POST /v1/memberships/invites
Invite someone. Past the included seats this is billed, never blocked
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| string (email) | required | ||
| role | "owner" | "admin" | "member" | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| membership | object | required | |
| membership.id | string | required | |
| membership.object | "membership" | required | |
| membership.user_id | string | required | |
| membership.email | string | required | |
| membership.display_name | string | null | required | |
| membership.role | "owner" | "admin" | "member" | required | |
| membership.status | "active" | "invited" | "removed" | required | |
| membership.invited_at | string (date-time) | null | required | |
| membership.invited_by | string | null | required | |
| membership.invited_by_name | string | null | required | |
| membership.invited_by_email | string | null | required | |
| membership.invite_expires_at | string (date-time) | null | required | |
| membership.accepted_at | string (date-time) | null | required | |
| membership.invite_expired | boolean | required | |
| seats | object | required | |
| seats.seats_used | integer | required | |
| seats.seats_included | integer | null | required | |
| seats.pending_invites | integer | required | |
| seats.next_invoice_delta_minor | string | required |
DELETE /v1/memberships/invites/{id}
Revoke a pending invite
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "membership" | required | |
| status | "removed" | required |
GET /v1/memberships/join-requests
People on your email domain waiting to be let in
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "join_request" | required | |
| data[].email | string | required | |
| data[].name | string | required | |
| data[].domain | string | required | |
| data[].status | "pending" | "approved" | "declined" | required | |
| data[].requested_at | string (date-time) | required | |
| data[].decided_at | string (date-time) | null | required | |
| data[].decided_by | string | null | required |
POST /v1/memberships/join-requests/{id}/approve
Approve a join request: admits the person as a member, in one transaction
Scopes none; needs a credential
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| join_request | object | required | |
| join_request.id | string | required | |
| join_request.object | "join_request" | required | |
| join_request.email | string | required | |
| join_request.name | string | required | |
| join_request.domain | string | required | |
| join_request.status | "pending" | "approved" | "declined" | required | |
| join_request.requested_at | string (date-time) | required | |
| join_request.decided_at | string (date-time) | null | required | |
| join_request.decided_by | string | null | required | |
| membership | object | required | |
| membership.id | string | required | |
| membership.object | "membership" | required | |
| membership.user_id | string | required | |
| membership.email | string | required | |
| membership.display_name | string | null | required | |
| membership.role | "owner" | "admin" | "member" | required | |
| membership.status | "active" | "invited" | "removed" | required | |
| membership.invited_at | string (date-time) | null | required | |
| membership.invited_by | string | null | required | |
| membership.invited_by_name | string | null | required | |
| membership.invited_by_email | string | null | required | |
| membership.invite_expires_at | string (date-time) | null | required | |
| membership.accepted_at | string (date-time) | null | required | |
| membership.invite_expired | boolean | required | |
| seats | object | required | |
| seats.seats_used | integer | required | |
| seats.seats_included | integer | null | required | |
| seats.pending_invites | integer | required | |
| seats.next_invoice_delta_minor | string | required |
POST /v1/memberships/join-requests/{id}/decline
Decline a join request. The requester is told, never who decided
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "join_request" | required | |
| string | required | ||
| name | string | required | |
| domain | string | required | |
| status | "pending" | "approved" | "declined" | required | |
| requested_at | string (date-time) | required | |
| decided_at | string (date-time) | null | required | |
| decided_by | string | null | required |
GET /v1/memberships/me/access-requests
When you asked the owners and admins for access to numbers, or null
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| asked_at | string (date-time) | null | required |
POST /v1/memberships/me/access-requests
Ask the owners and admins for access to numbers. Asking again is the same request
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| asked_at | string (date-time) | null | required |
POST /v1/memberships/transfer-owner
Promote the target and demote the caller, in ONE transaction
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| membership_id | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| new_owner | object | required | |
| new_owner.id | string | required | |
| new_owner.object | "membership" | required | |
| new_owner.user_id | string | required | |
| new_owner.email | string | required | |
| new_owner.display_name | string | null | required | |
| new_owner.role | "owner" | "admin" | "member" | required | |
| new_owner.status | "active" | "invited" | "removed" | required | |
| new_owner.invited_at | string (date-time) | null | required | |
| new_owner.invited_by | string | null | required | |
| new_owner.invited_by_name | string | null | required | |
| new_owner.invited_by_email | string | null | required | |
| new_owner.invite_expires_at | string (date-time) | null | required | |
| new_owner.accepted_at | string (date-time) | null | required | |
| new_owner.invite_expired | boolean | required | |
| previous_owner | object | required | |
| previous_owner.id | string | required | |
| previous_owner.object | "membership" | required | |
| previous_owner.user_id | string | required | |
| previous_owner.email | string | required | |
| previous_owner.display_name | string | null | required | |
| previous_owner.role | "owner" | "admin" | "member" | required | |
| previous_owner.status | "active" | "invited" | "removed" | required | |
| previous_owner.invited_at | string (date-time) | null | required | |
| previous_owner.invited_by | string | null | required | |
| previous_owner.invited_by_name | string | null | required | |
| previous_owner.invited_by_email | string | null | required | |
| previous_owner.invite_expires_at | string (date-time) | null | required | |
| previous_owner.accepted_at | string (date-time) | null | required | |
| previous_owner.invite_expired | boolean | required |
/v1/messages
GET /v1/messages
List messages; with q, search the text of every message instead (page size 20)
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| order | query | "asc" | "desc" | optional | asc (the default) or desc. Not used with q: a search is newest first. |
| conversation_id | query | string | optional | |
| contact_id | query | string | optional | |
| connection_id | query | string | string[] | optional | A connection's id; repeat it for several numbers. |
| direction | query | string | optional | |
| source | query | string | optional | |
| status | query | string | optional | |
| q | query | string | optional | Search the text of the workspace's messages for this, newest first; the answer is hits ({message_id, conversation_id, connection_id, snippet, created_at}), 20 to a page. Messages of a number whose delivery_mode is direct are never searched. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "message" | required | |
| data[].conversation_id | string | required | |
| data[].contact_id | string | required | |
| data[].contact | object | required | |
| data[].contact.id | string | required | |
| data[].contact.bsuid | string | null | required | |
| data[].contact.phone | string | null | required | |
| data[].contact.username | string | null | required | |
| data[].contact.name | string | required | |
| data[].connection_id | string | required | |
| data[].wamid | string | null | required | |
| data[].direction | "inbound" | "outbound" | required | |
| data[].source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| data[].status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| data[].type | string | required | |
| data[].content | object | required | |
| data[].location | object | null | required | |
| data[].location.latitude | number | required | |
| data[].location.longitude | number | required | |
| data[].location.name | string | null | required | |
| data[].location.address | string | null | required | |
| data[].location.url | string | null | required | |
| data[].contact_cards | object[] | null | required | |
| data[].contact_cards[].name | string | null | required | |
| data[].contact_cards[].phones | object[] | required | |
| data[].contact_cards[].emails | object[] | required | |
| data[].contact_cards[].org | object | null | required | |
| data[].label | string | null | required | |
| data[].forwarded | boolean | required | |
| data[].frequently_forwarded | boolean | required | |
| data[].tap | object | null | required | |
| data[].tap.kind | "button_reply" | "list_reply" | "button" | required | |
| data[].tap.id | string | required | |
| data[].tap.title | string | required | |
| data[].tap.description | string | null | required | |
| data[].template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| data[].template_text.header | string | null | required | |
| data[].template_text.body | string | null | required | |
| data[].template_text.footer | string | null | required | |
| data[].template_text.buttons | string[] | required | |
| data[].reply_to | object | null | required | |
| data[].reply_to.id | string | null | required | |
| data[].reply_to.wamid | string | required | |
| data[].reply_to.kind | string | null | required | |
| data[].reply_to.from | "contact" | "business" | null | required | |
| data[].reply_to.preview | string | required | |
| data[].reactions | object[] | required | |
| data[].reactions[].emoji | string | required | |
| data[].reactions[].direction | "inbound" | "outbound" | required | |
| data[].reactions[].message_id | string | required | |
| data[].reactions[].created_at | string (date-time) | required | |
| data[].metadata | object | required | |
| data[].created_at | string (date-time) | required | |
| data[].wa_timestamp | string (date-time) | required | |
| data[].is_imported | boolean | required | |
| data[].media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| data[].media_expires_at | string (date-time) | null | required | |
| data[].media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| data[].media.id | string | null | required | |
| data[].media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| data[].media.mime_type | string | null | required | |
| data[].media.size_bytes | integer | null | required | |
| data[].media.filename | string | null | required | |
| data[].media.caption | string | null | required | |
| data[].media.animated | boolean | null | required | |
| data[].media.status | "pending" | "ready" | "failed" | required | |
| data[].media.failure_reason | string | null | required | |
| data[].transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| data[].transcript.status | "ready" | "failed" | "skipped" | required | |
| data[].transcript.text | string | null | required | |
| data[].transcript.language | string | null | required | |
| data[].transcript.model | string | null | required | |
| data[].transcript.failure_reason | string | null | required | |
| data[].client_token | string | null | required | |
| data[].author_user_id | string | null | required | |
| data[].automation_run_id | string | null | required | |
| data[].error_code | integer | null | required | |
| data[].error_title | string | null | required | |
| data[].error_detail | string | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/messages
Send a message
Scopes messages:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | optional | The WhatsApp number the message goes out from. Optional with a connection key, which already names it. A contact belongs to one connection: a contact_id, a to of ct_… or a conversation_id on a different connection from this one is refused with 409 contact_belongs_to_other_connection, whose message names both numbers and whose body carries contact_connection_id and connection_id. |
| to | string | optional | |
| contact_id | string | optional | |
| conversation_id | string | optional | |
| type | "text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | … | required | |
| text | object | optional | |
| image | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| audio | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| video | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| document | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| sticker | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| location | object | optional | |
| contacts | object[] | optional | |
| interactive | object | optional | Meta's interactive object. Checked against WhatsApp's limits before anything is written, and refused in words naming the field: a reply-button message (type "button", up to 3 buttons), a list message (type "list"), a catalogue message (type "catalog_message": body up to 1024 characters, footer up to 60, optional action.parameters.thumbnail_product_retailer_id), a single-product message (type "product": action.product_retailer_id, optional body and footer, no header) and a multi-product message (type "product_list": a text header up to 60 characters, a body, action.sections of product_items, at most 10 sections and 30 products, a title up to 24 characters on each section when there is more than one). A product message may leave out action.catalog_id, and Cosend sends the catalogue linked to the number's WhatsApp Business account in Meta's Commerce Manager; a catalogue message, and a product message without catalog_id, are refused in words when the number has none linked. A catalog_id you give is sent as given. Like every free-form message, they go only inside the 24-hour customer service window. Any other interactive type goes to WhatsApp unchanged. |
| interactive.type | "button" | required | |
| interactive.header | object | optional | |
| interactive.header.type | "text" | required | |
| interactive.header.text | string | required | |
| interactive.header.image | object | required | |
| interactive.header.video | object | required | |
| interactive.header.document | object | required | |
| interactive.body | object | required | |
| interactive.body.text | string | required | |
| interactive.footer | object | optional | |
| interactive.footer.text | string | required | |
| interactive.action | object | required | |
| interactive.action.buttons | object[] | required | |
| template | object | optional | Meta's template object: {"name", "language": {"code"}, "components"}. A header parameter of type image, video or document may name a file uploaded with POST /v1/media by its id ({"type": "image", "image": {"id": "med_…"}}): Cosend uploads it to Meta for this number and sends Meta's id; a link, or a media id from Meta, is sent as it is. A location parameter ({"type": "location", "location": {"latitude", "longitude", "name", "address"}}) needs a latitude from -90 to 90 and a longitude from -180 to 180, in decimal degrees; name and address are text. |
| reaction | object | optional | |
| reaction.message_id | string | required | |
| reaction.emoji | string | required | |
| reply_to | string | null | optional | |
| metadata | object | optional | |
| source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | optional | Who is sending. Left out, it is agent for a person (the dashboard Inbox, or an MCP client acting for one) and api for an API key. |
| send_mode | "auto" | "strict" | optional | |
| fallback_template | object | optional | |
| fallback_template.name | string | required | |
| fallback_template.language | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "message" | required | |
| conversation_id | string | required | |
| contact_id | string | required | |
| contact | object | required | |
| contact.id | string | required | |
| contact.bsuid | string | null | required | |
| contact.phone | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | required | |
| connection_id | string | required | |
| wamid | string | null | required | |
| direction | "inbound" | "outbound" | required | |
| source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| type | string | required | |
| content | object | required | |
| location | object | null | required | |
| location.latitude | number | required | |
| location.longitude | number | required | |
| location.name | string | null | required | |
| location.address | string | null | required | |
| location.url | string | null | required | |
| contact_cards | object[] | null | required | |
| contact_cards[].name | string | null | required | |
| contact_cards[].phones | object[] | required | |
| contact_cards[].phones[].phone | string | null | required | |
| contact_cards[].phones[].wa_id | string | null | required | |
| contact_cards[].phones[].type | string | null | required | |
| contact_cards[].emails | object[] | required | |
| contact_cards[].emails[].email | string | required | |
| contact_cards[].emails[].type | string | null | required | |
| contact_cards[].org | object | null | required | |
| contact_cards[].org.company | string | null | required | |
| contact_cards[].org.department | string | null | required | |
| contact_cards[].org.title | string | null | required | |
| label | string | null | required | |
| forwarded | boolean | required | |
| frequently_forwarded | boolean | required | |
| tap | object | null | required | |
| tap.kind | "button_reply" | "list_reply" | "button" | required | |
| tap.id | string | required | |
| tap.title | string | required | |
| tap.description | string | null | required | |
| template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| template_text.header | string | null | required | |
| template_text.body | string | null | required | |
| template_text.footer | string | null | required | |
| template_text.buttons | string[] | required | |
| reply_to | object | null | required | |
| reply_to.id | string | null | required | |
| reply_to.wamid | string | required | |
| reply_to.kind | string | null | required | |
| reply_to.from | "contact" | "business" | null | required | |
| reply_to.preview | string | required | |
| reactions | object[] | required | |
| reactions[].emoji | string | required | |
| reactions[].direction | "inbound" | "outbound" | required | |
| reactions[].message_id | string | required | |
| reactions[].created_at | string (date-time) | required | |
| metadata | object | required | |
| created_at | string (date-time) | required | |
| wa_timestamp | string (date-time) | required | |
| is_imported | boolean | required | |
| media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| media_expires_at | string (date-time) | null | required | |
| media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| media.id | string | null | required | |
| media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| media.mime_type | string | null | required | |
| media.size_bytes | integer | null | required | |
| media.filename | string | null | required | |
| media.caption | string | null | required | |
| media.animated | boolean | null | required | |
| media.status | "pending" | "ready" | "failed" | required | |
| media.failure_reason | string | null | required | |
| transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| transcript.status | "ready" | "failed" | "skipped" | required | |
| transcript.text | string | null | required | |
| transcript.language | string | null | required | |
| transcript.model | string | null | required | |
| transcript.failure_reason | string | null | required | |
| client_token | string | null | required | |
| author_user_id | string | null | required | |
| automation_run_id | string | null | required | |
| error_code | integer | null | required | |
| error_title | string | null | required | |
| error_detail | string | null | required | |
| send_mode_used | "freeform" | "template" | required |
Errors contact_belongs_to_other_connection conversation_locked
GET /v1/messages/{id}
One message, by msg_… id or by wamid
Scopes messages:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "message" | required | |
| conversation_id | string | required | |
| contact_id | string | required | |
| contact | object | required | |
| contact.id | string | required | |
| contact.bsuid | string | null | required | |
| contact.phone | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | required | |
| connection_id | string | required | |
| wamid | string | null | required | |
| direction | "inbound" | "outbound" | required | |
| source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| type | string | required | |
| content | object | required | |
| location | object | null | required | |
| location.latitude | number | required | |
| location.longitude | number | required | |
| location.name | string | null | required | |
| location.address | string | null | required | |
| location.url | string | null | required | |
| contact_cards | object[] | null | required | |
| contact_cards[].name | string | null | required | |
| contact_cards[].phones | object[] | required | |
| contact_cards[].phones[].phone | string | null | required | |
| contact_cards[].phones[].wa_id | string | null | required | |
| contact_cards[].phones[].type | string | null | required | |
| contact_cards[].emails | object[] | required | |
| contact_cards[].emails[].email | string | required | |
| contact_cards[].emails[].type | string | null | required | |
| contact_cards[].org | object | null | required | |
| contact_cards[].org.company | string | null | required | |
| contact_cards[].org.department | string | null | required | |
| contact_cards[].org.title | string | null | required | |
| label | string | null | required | |
| forwarded | boolean | required | |
| frequently_forwarded | boolean | required | |
| tap | object | null | required | |
| tap.kind | "button_reply" | "list_reply" | "button" | required | |
| tap.id | string | required | |
| tap.title | string | required | |
| tap.description | string | null | required | |
| template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| template_text.header | string | null | required | |
| template_text.body | string | null | required | |
| template_text.footer | string | null | required | |
| template_text.buttons | string[] | required | |
| reply_to | object | null | required | |
| reply_to.id | string | null | required | |
| reply_to.wamid | string | required | |
| reply_to.kind | string | null | required | |
| reply_to.from | "contact" | "business" | null | required | |
| reply_to.preview | string | required | |
| reactions | object[] | required | |
| reactions[].emoji | string | required | |
| reactions[].direction | "inbound" | "outbound" | required | |
| reactions[].message_id | string | required | |
| reactions[].created_at | string (date-time) | required | |
| metadata | object | required | |
| created_at | string (date-time) | required | |
| wa_timestamp | string (date-time) | required | |
| is_imported | boolean | required | |
| media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| media_expires_at | string (date-time) | null | required | |
| media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| media.id | string | null | required | |
| media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| media.mime_type | string | null | required | |
| media.size_bytes | integer | null | required | |
| media.filename | string | null | required | |
| media.caption | string | null | required | |
| media.animated | boolean | null | required | |
| media.status | "pending" | "ready" | "failed" | required | |
| media.failure_reason | string | null | required | |
| transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| transcript.status | "ready" | "failed" | "skipped" | required | |
| transcript.text | string | null | required | |
| transcript.language | string | null | required | |
| transcript.model | string | null | required | |
| transcript.failure_reason | string | null | required | |
| client_token | string | null | required | |
| author_user_id | string | null | required | |
| automation_run_id | string | null | required | |
| error_code | integer | null | required | |
| error_title | string | null | required | |
| error_detail | string | null | required |
POST /v1/messages/{id}/reactions
React to a message; an empty emoji removes the reaction
Scopes messages:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| emoji | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "message" | required | |
| conversation_id | string | required | |
| contact_id | string | required | |
| contact | object | required | |
| contact.id | string | required | |
| contact.bsuid | string | null | required | |
| contact.phone | string | null | required | |
| contact.username | string | null | required | |
| contact.name | string | required | |
| connection_id | string | required | |
| wamid | string | null | required | |
| direction | "inbound" | "outbound" | required | |
| source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | required | |
| status | "queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | … | required | |
| type | string | required | |
| content | object | required | |
| location | object | null | required | |
| location.latitude | number | required | |
| location.longitude | number | required | |
| location.name | string | null | required | |
| location.address | string | null | required | |
| location.url | string | null | required | |
| contact_cards | object[] | null | required | |
| contact_cards[].name | string | null | required | |
| contact_cards[].phones | object[] | required | |
| contact_cards[].phones[].phone | string | null | required | |
| contact_cards[].phones[].wa_id | string | null | required | |
| contact_cards[].phones[].type | string | null | required | |
| contact_cards[].emails | object[] | required | |
| contact_cards[].emails[].email | string | required | |
| contact_cards[].emails[].type | string | null | required | |
| contact_cards[].org | object | null | required | |
| contact_cards[].org.company | string | null | required | |
| contact_cards[].org.department | string | null | required | |
| contact_cards[].org.title | string | null | required | |
| label | string | null | required | |
| forwarded | boolean | required | |
| frequently_forwarded | boolean | required | |
| tap | object | null | required | |
| tap.kind | "button_reply" | "list_reply" | "button" | required | |
| tap.id | string | required | |
| tap.title | string | required | |
| tap.description | string | null | required | |
| template_text | object | null | required | The words a template message showed the customer: the header and body with the values the send filled in, the footer, and the button labels. Rendered from the template Cosend holds and the values the send used. An authentication template never shows its one-time code: the placeholder stays as written. When Cosend no longer holds the template, body is the text stored when it was sent and the rest is empty. null on every other message, on a template Cosend has no words for, and on a direct-mode connection. |
| template_text.header | string | null | required | |
| template_text.body | string | null | required | |
| template_text.footer | string | null | required | |
| template_text.buttons | string[] | required | |
| reply_to | object | null | required | |
| reply_to.id | string | null | required | |
| reply_to.wamid | string | required | |
| reply_to.kind | string | null | required | |
| reply_to.from | "contact" | "business" | null | required | |
| reply_to.preview | string | required | |
| reactions | object[] | required | |
| reactions[].emoji | string | required | |
| reactions[].direction | "inbound" | "outbound" | required | |
| reactions[].message_id | string | required | |
| reactions[].created_at | string (date-time) | required | |
| metadata | object | required | |
| created_at | string (date-time) | required | |
| wa_timestamp | string (date-time) | required | |
| is_imported | boolean | required | |
| media_state | "none" | "pending" | "stored" | "expired" | "unavailable" | required | |
| media_expires_at | string (date-time) | null | required | |
| media | object | null | required | The file this message carries. status is pending while Cosend fetches it from Meta, ready once it is stored (open it with GET /v1/media/{id} or /v1/media/{id}/content; id is null until then), and failed with failure_reason saying why in plain words. kind is voice for a WhatsApp voice note. null on a message with no file Cosend keeps. |
| media.id | string | null | required | |
| media.kind | "image" | "video" | "audio" | "voice" | "document" | "sticker" | required | |
| media.mime_type | string | null | required | |
| media.size_bytes | integer | null | required | |
| media.filename | string | null | required | |
| media.caption | string | null | required | |
| media.animated | boolean | null | required | |
| media.status | "pending" | "ready" | "failed" | required | |
| media.failure_reason | string | null | required | |
| transcript | object | null | required | The words of an inbound voice note or audio file. null until Cosend has transcribed it, and on every other message. ready carries text; failed says why in failure_reason; skipped means transcription is not configured on this deployment. The recording in media is the same whatever this says. |
| transcript.status | "ready" | "failed" | "skipped" | required | |
| transcript.text | string | null | required | |
| transcript.language | string | null | required | |
| transcript.model | string | null | required | |
| transcript.failure_reason | string | null | required | |
| client_token | string | null | required | |
| author_user_id | string | null | required | |
| automation_run_id | string | null | required | |
| error_code | integer | null | required | |
| error_title | string | null | required | |
| error_detail | string | null | required |
POST /v1/messages/{id}/read
Mark read: shows blue ticks on the contact’s phone
Scopes messages:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| ok | true | required |
POST /v1/messages/{id}/translate
Translate a message into another language (a tag such as es or pt-BR); read back from the cache after the first time. Nothing is sent
Scopes messages:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| target_language | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "message_translation" | required | |
| message_id | string | required | |
| target_language | string | required | |
| text | string | required | |
| detected_language | string | null | required | |
| cached | boolean | required |
POST /v1/messages/batch
Send up to 100 messages, with a per-item result
Scopes messages:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| messages | object[] | required | |
| messages[].connection_id | string | optional | The WhatsApp number the message goes out from. Optional with a connection key, which already names it. A contact belongs to one connection: a contact_id, a to of ct_… or a conversation_id on a different connection from this one is refused with 409 contact_belongs_to_other_connection, whose message names both numbers and whose body carries contact_connection_id and connection_id. |
| messages[].to | string | optional | |
| messages[].contact_id | string | optional | |
| messages[].conversation_id | string | optional | |
| messages[].type | "text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | … | required | |
| messages[].text | object | optional | |
| messages[].image | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| messages[].audio | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| messages[].video | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| messages[].document | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| messages[].sticker | object | optional | Meta's media object. id may be a Cosend media id (med_…, from POST /v1/media): Cosend uploads the file to WhatsApp and sends it by WhatsApp's id, and the message's media block then names the file. With a Cosend id, caption is allowed on image, video and document (not on audio or sticker), and filename on document only (it defaults to the uploaded name). Meta's own id or link goes to WhatsApp unchanged. |
| messages[].location | object | optional | |
| messages[].contacts | object[] | optional | |
| messages[].interactive | object | optional | Meta's interactive object. Checked against WhatsApp's limits before anything is written, and refused in words naming the field: a reply-button message (type "button", up to 3 buttons), a list message (type "list"), a catalogue message (type "catalog_message": body up to 1024 characters, footer up to 60, optional action.parameters.thumbnail_product_retailer_id), a single-product message (type "product": action.product_retailer_id, optional body and footer, no header) and a multi-product message (type "product_list": a text header up to 60 characters, a body, action.sections of product_items, at most 10 sections and 30 products, a title up to 24 characters on each section when there is more than one). A product message may leave out action.catalog_id, and Cosend sends the catalogue linked to the number's WhatsApp Business account in Meta's Commerce Manager; a catalogue message, and a product message without catalog_id, are refused in words when the number has none linked. A catalog_id you give is sent as given. Like every free-form message, they go only inside the 24-hour customer service window. Any other interactive type goes to WhatsApp unchanged. |
| messages[].interactive.type | "button" | required | |
| messages[].interactive.header | object | optional | |
| messages[].interactive.body | object | required | |
| messages[].interactive.footer | object | optional | |
| messages[].interactive.action | object | required | |
| messages[].template | object | optional | Meta's template object: {"name", "language": {"code"}, "components"}. A header parameter of type image, video or document may name a file uploaded with POST /v1/media by its id ({"type": "image", "image": {"id": "med_…"}}): Cosend uploads it to Meta for this number and sends Meta's id; a link, or a media id from Meta, is sent as it is. A location parameter ({"type": "location", "location": {"latitude", "longitude", "name", "address"}}) needs a latitude from -90 to 90 and a longitude from -180 to 180, in decimal degrees; name and address are text. |
| messages[].reaction | object | optional | |
| messages[].reaction.message_id | string | required | |
| messages[].reaction.emoji | string | required | |
| messages[].reply_to | string | null | optional | |
| messages[].metadata | object | optional | |
| messages[].source | "customer" | "app" | "api" | "agent" | "automation" | "ai" | "notify" | "history" | … | optional | Who is sending. Left out, it is agent for a person (the dashboard Inbox, or an MCP client acting for one) and api for an API key. |
| messages[].send_mode | "auto" | "strict" | optional | |
| messages[].fallback_template | object | optional | |
| messages[].fallback_template.name | string | required | |
| messages[].fallback_template.language | string | required | |
| messages[].idempotency_key | string | optional |
Response 207
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| results | object[] | required | |
| results[].index | integer | required | |
| results[].status | integer | required | |
| results[].id | string | optional | |
| results[].error | object | optional | |
| results[].error.type | string | required | |
| results[].error.code | string | required | |
| results[].error.message | string | required |
/v1/notifications
GET /v1/notifications
List notifications
Scopes notifications:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| state | query | string | optional | |
| recipient | query | string | optional | |
| dedup_key | query | string | optional | |
| source_ref_id | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "notification" | required | |
| data[].connection_id | string | required | |
| data[].to | string | null | required | |
| data[].state | string | required | |
| data[].priority | integer | required | |
| data[].send_mode | string | required | |
| data[].send_mode_used | string | null | required | |
| data[].template_name | string | null | required | |
| data[].template_language | string | null | required | |
| data[].dedup_key | string | null | required | |
| data[].deduplicated | boolean | required | |
| data[].scheduled_for | string (date-time) | null | required | |
| data[].expires_at | string (date-time) | null | required | |
| data[].source_ref | any | null | required | |
| data[].wamid | string | null | required | |
| data[].latest_status | string | null | required | |
| data[].suppress_reason | string | null | required | |
| data[].failure_class | string | null | required | |
| data[].error_code | integer | null | required | |
| data[].billable | boolean | null | required | |
| data[].attempts | integer | required | |
| data[].replay_of | string | null | required | |
| data[].sent_at | string (date-time) | null | required | |
| data[].delivered_at | string (date-time) | null | required | |
| data[].read_at | string (date-time) | null | required | |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/notifications
Queue one notification
Scopes notifications:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| to | string | required | |
| send_mode | "auto" | "template" | "freeform" | optional | |
| template | object | optional | |
| template.name | string | required | |
| template.language | string | required | |
| template.params | object | optional | |
| fallback_body | object | optional | |
| fallback_body.type | string | required | |
| scheduled_for | string (date-time) | null | optional | |
| expires_at | string (date-time) | null | optional | |
| dedup_key | string | null | optional | |
| priority | integer | optional | |
| source_ref | object | null | optional | |
| source_ref.type | string | required | |
| source_ref.id | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notification" | required | |
| connection_id | string | required | |
| to | string | null | required | |
| state | string | required | |
| priority | integer | required | |
| send_mode | string | required | |
| send_mode_used | string | null | required | |
| template_name | string | null | required | |
| template_language | string | null | required | |
| dedup_key | string | null | required | |
| deduplicated | boolean | required | |
| scheduled_for | string (date-time) | null | required | |
| expires_at | string (date-time) | null | required | |
| source_ref | any | null | required | |
| wamid | string | null | required | |
| latest_status | string | null | required | |
| suppress_reason | string | null | required | |
| failure_class | string | null | required | |
| error_code | integer | null | required | |
| billable | boolean | null | required | |
| attempts | integer | required | |
| replay_of | string | null | required | |
| sent_at | string (date-time) | null | required | |
| delivered_at | string (date-time) | null | required | |
| read_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notification" | required | |
| connection_id | string | required | |
| to | string | null | required | |
| state | string | required | |
| priority | integer | required | |
| send_mode | string | required | |
| send_mode_used | string | null | required | |
| template_name | string | null | required | |
| template_language | string | null | required | |
| dedup_key | string | null | required | |
| deduplicated | boolean | required | |
| scheduled_for | string (date-time) | null | required | |
| expires_at | string (date-time) | null | required | |
| source_ref | any | null | required | |
| wamid | string | null | required | |
| latest_status | string | null | required | |
| suppress_reason | string | null | required | |
| failure_class | string | null | required | |
| error_code | integer | null | required | |
| billable | boolean | null | required | |
| attempts | integer | required | |
| replay_of | string | null | required | |
| sent_at | string (date-time) | null | required | |
| delivered_at | string (date-time) | null | required | |
| read_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
GET /v1/notifications/{id}
One notification, with its receipt timeline
Scopes notifications:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notification" | required | |
| connection_id | string | required | |
| to | string | null | required | |
| state | string | required | |
| priority | integer | required | |
| send_mode | string | required | |
| send_mode_used | string | null | required | |
| template_name | string | null | required | |
| template_language | string | null | required | |
| dedup_key | string | null | required | |
| deduplicated | boolean | required | |
| scheduled_for | string (date-time) | null | required | |
| expires_at | string (date-time) | null | required | |
| source_ref | any | null | required | |
| wamid | string | null | required | |
| latest_status | string | null | required | |
| suppress_reason | string | null | required | |
| failure_class | string | null | required | |
| error_code | integer | null | required | |
| billable | boolean | null | required | |
| attempts | integer | required | |
| replay_of | string | null | required | |
| sent_at | string (date-time) | null | required | |
| delivered_at | string (date-time) | null | required | |
| read_at | string (date-time) | null | required | |
| created_at | string (date-time) | required | |
| receipts | object[] | required | |
| receipts[].object | "notify_receipt" | required | |
| receipts[].status | string | required | |
| receipts[].status_at | string (date-time) | required | |
| receipts[].received_at | string (date-time) | required | |
| receipts[].pricing_billable | boolean | null | required | |
| receipts[].pricing_category | string | null | required | |
| receipts[].error_code | integer | null | required | |
| receipts[].error_title | string | null | required |
POST /v1/notifications/{id}/cancel
Cancel a scheduled or queued notification
Scopes notifications:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notification" | required | |
| connection_id | string | required | |
| to | string | null | required | |
| state | string | required | |
| priority | integer | required | |
| send_mode | string | required | |
| send_mode_used | string | null | required | |
| template_name | string | null | required | |
| template_language | string | null | required | |
| dedup_key | string | null | required | |
| deduplicated | boolean | required | |
| scheduled_for | string (date-time) | null | required | |
| expires_at | string (date-time) | null | required | |
| source_ref | any | null | required | |
| wamid | string | null | required | |
| latest_status | string | null | required | |
| suppress_reason | string | null | required | |
| failure_class | string | null | required | |
| error_code | integer | null | required | |
| billable | boolean | null | required | |
| attempts | integer | required | |
| replay_of | string | null | required | |
| sent_at | string (date-time) | null | required | |
| delivered_at | string (date-time) | null | required | |
| read_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
POST /v1/notifications/{id}/replay
Clone a dead-lettered notification as a new attempt
Scopes notifications:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notification" | required | |
| connection_id | string | required | |
| to | string | null | required | |
| state | string | required | |
| priority | integer | required | |
| send_mode | string | required | |
| send_mode_used | string | null | required | |
| template_name | string | null | required | |
| template_language | string | null | required | |
| dedup_key | string | null | required | |
| deduplicated | boolean | required | |
| scheduled_for | string (date-time) | null | required | |
| expires_at | string (date-time) | null | required | |
| source_ref | any | null | required | |
| wamid | string | null | required | |
| latest_status | string | null | required | |
| suppress_reason | string | null | required | |
| failure_class | string | null | required | |
| error_code | integer | null | required | |
| billable | boolean | null | required | |
| attempts | integer | required | |
| replay_of | string | null | required | |
| sent_at | string (date-time) | null | required | |
| delivered_at | string (date-time) | null | required | |
| read_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
POST /v1/notifications/batch
Queue up to 500 notifications
Scopes notifications:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| notifications | object[] | required | |
| notifications[].connection_id | string | required | |
| notifications[].to | string | required | |
| notifications[].send_mode | "auto" | "template" | "freeform" | optional | |
| notifications[].template | object | optional | |
| notifications[].template.name | string | required | |
| notifications[].template.language | string | required | |
| notifications[].template.params | object | optional | |
| notifications[].fallback_body | object | optional | |
| notifications[].fallback_body.type | string | required | |
| notifications[].scheduled_for | string (date-time) | null | optional | |
| notifications[].expires_at | string (date-time) | null | optional | |
| notifications[].dedup_key | string | null | optional | |
| notifications[].priority | integer | optional | |
| notifications[].source_ref | object | null | optional | |
| notifications[].source_ref.type | string | required | |
| notifications[].source_ref.id | string | required |
Response 207
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "batch" | required | |
| results | object[] | required | |
| results[].index | integer | required | |
| results[].status | integer | required | |
| results[].notification | object | optional | |
| results[].notification.id | string | required | |
| results[].notification.object | "notification" | required | |
| results[].notification.connection_id | string | required | |
| results[].notification.to | string | null | required | |
| results[].notification.state | string | required | |
| results[].notification.priority | integer | required | |
| results[].notification.send_mode | string | required | |
| results[].notification.send_mode_used | string | null | required | |
| results[].notification.template_name | string | null | required | |
| results[].notification.template_language | string | null | required | |
| results[].notification.dedup_key | string | null | required | |
| results[].notification.deduplicated | boolean | required | |
| results[].notification.scheduled_for | string (date-time) | null | required | |
| results[].notification.expires_at | string (date-time) | null | required | |
| results[].notification.source_ref | any | null | required | |
| results[].notification.wamid | string | null | required | |
| results[].notification.latest_status | string | null | required | |
| results[].notification.suppress_reason | string | null | required | |
| results[].notification.failure_class | string | null | required | |
| results[].notification.error_code | integer | null | required | |
| results[].notification.billable | boolean | null | required | |
| results[].notification.attempts | integer | required | |
| results[].notification.replay_of | string | null | required | |
| results[].notification.sent_at | string (date-time) | null | required | |
| results[].notification.delivered_at | string (date-time) | null | required | |
| results[].notification.read_at | string (date-time) | null | required | |
| results[].notification.created_at | string (date-time) | required | |
| results[].error | object | optional | |
| results[].error.code | string | required | |
| results[].error.message | string | required |
/v1/organizations
GET /v1/organizations
The organizations this user belongs to, for an organization switcher
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "organization" | required | |
| data[].name | string | required | |
| data[].slug | string | required | |
| data[].timezone | string | required | |
| data[].country | string | null | required | |
| data[].role | "owner" | "admin" | "member" | required | |
| data[].status | "active" | "invited" | required | |
| data[].attention | object | null | required | |
| data[].attention.approvals_pending | integer | required |
POST /v1/organizations
Create an organization; the caller becomes its owner
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "organization" | required | |
| name | string | required | |
| slug | string | required | |
| timezone | string | required | |
| country | string | null | required | |
| role | "owner" | "admin" | "member" | required | |
| status | "active" | "invited" | required |
PATCH /v1/organizations/{id}
Rename an organization (owner), or change its time zone (owner or admin), as an IANA name
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | |
| timezone | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "organization" | required | |
| name | string | required | |
| timezone | string | required |
/v1/recipes
GET /v1/recipes
The published recipe library. Cards, and no ir. ?connection_id= marks the recipes that cannot work on that number
Scopes automations:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| connection_id | query | string | optional | One WhatsApp number's (connection id). A recipe that cannot work on that number carries unavailable. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].object | "recipe" | required | |
| data[].slug | string | required | |
| data[].version | integer | required | |
| data[].name | string | required | |
| data[].description | string | null | required | |
| data[].category | string | null | required | |
| data[].inputs_schema | object | required | The form, as a JSON Schema. A template field (picker: "template") carries ready_made: {template, owner_adds?}. template is the body POST /v1/templates takes, less connection_id: display_name, category, language, components and, when it has variables, example_values. owner_adds, when present, is [{pointer, title, description}]: what only the owner can give, each a JSON pointer into template (the review request's link), which the create refuses until it is there. A language field (picker: "language") carries templates, the template fields it is for: its value is a language every one of the templates chosen in them is approved in on the number, as GET /v1/templates?connection_id= lists them (name, language, status), or "" for none chosen. Another is refused at save with automation_invalid. |
| data[].inputs_schema.order | string[] | optional | The order the settings are shown in: the name of every setting in properties, first to last, as declared. properties itself comes back in no particular order. Every recipe in the library, and every automation that declares settings, carries it. |
| data[].inputs_schema.properties | object | optional | |
| data[].sheet | object | null | required | The Google Sheet this recipe reads: its name, its tab, its columns and an example row. Send it as it is to POST /v1/connector-accounts/{id}/sheets to make that sheet. null: the recipe reads no sheet. |
| data[].sheet.title | string | required | The spreadsheet’s name. |
| data[].sheet.tab | string | required | The one tab’s name. |
| data[].sheet.columns | object[] | required | The header row the recipe reads, first column first. |
| data[].sheet.example_row | string[] | optional | The example row under the header, as wide as columns. Absent: header row only. |
| data[].unavailable | object | optional | Present only when the request named a WhatsApp number (?connection_id=) and the recipe cannot work on it. reason is needs_cloud_api: the number is also in the WhatsApp Business app, and the recipe needs WhatsApp calling, which Meta offers only on a number that runs on Cloud API. message says so in words. Setting it up on that number is refused with 403 not_supported_on_coexistence, and so is switching it on. Absent: available on that number, or no number was named. |
| data[].unavailable.reason | "needs_cloud_api" | required | |
| data[].unavailable.message | string | required | |
| data[].multiple | object | optional | Present when the recipe can be set up several times on one WhatsApp number, one copy per source: by is the setting that names the source (a sheet, a tag, a shop, a calendar) and label is that setting's title. A second copy for a source another copy on the number has is refused with 409 recipe_source_taken. Absent: once per number. |
| data[].multiple.by | string | required | |
| data[].multiple.label | string | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
Errors automation_invalid not_supported_on_coexistence recipe_source_taken
POST /v1/recipes/{slug}/instantiate
Instantiate a recipe into an IR. Substituted and validated server-side
Scopes automations:read
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| params | object | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| ir | object | required | |
| validation | object | required | |
| validation.valid | boolean | required | |
| validation.compiler_version | string | required | |
| validation.catalogue_version | string | required | |
| validation.errors | object[] | required | |
| validation.errors[].pointer | string | required | |
| validation.errors[].code | string | required | |
| validation.errors[].message | string | required | |
| validation.errors[].fix | object | optional | |
| validation.errors[].detail | string | optional | When message is in plain words for the owner, the technical text it was said in place of, for Details. |
| validation.warnings | object[] | required | |
| validation.warnings[].pointer | string | required | |
| validation.warnings[].code | string | required | |
| validation.warnings[].message | string | required | |
| validation.warnings[].fix | object | optional | |
| validation.warnings[].detail | string | optional | When message is in plain words for the owner, the technical text it was said in place of, for Details. |
| validation.derived | object | optional | |
| validation.derived.acts | string[] | required | |
| validation.derived.budget | object | required | |
| validation.derived.segment_count | integer | required |
/v1/runs
GET /v1/runs
Runs, filterable by automation, status, WhatsApp number and date
Scopes runs:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional | |
| automation_id | query | string | optional | |
| status | query | string | optional | |
| connection_id | query | string | string[] | optional | WhatsApp numbers (connection ids), repeatable: the rows for any of them. Automations: those on it, and those made before numbers were required, which have none and run on every number. Runs: those that ran on it, and those that name no number of their own whose automation is one of these. |
| created_after | query | string (date-time) | optional | Only runs triggered at or after this instant: their created_at, not started_at. ISO-8601 with its timezone (send a + offset as %2B), read to the millisecond. |
| created_before | query | string (date-time) | optional | Only runs triggered before this instant: their created_at, not started_at. ISO-8601 with its timezone (send a + offset as %2B), read to the millisecond. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "run" | required | |
| data[].automation_id | string | required | |
| data[].automation_name | string | required | The name its owner knows the automation by: for one installed with another, that one’s name, and a deleted automation keeps its own. So a run is named even when its automation is not in the automations list. |
| data[].connection_id | string | null | required | |
| data[].version_no | integer | required | |
| data[].status | "queued" | "running" | "waiting" | "success" | "failure" | "cancelled" | "timeout" | "skipped" | … | required | |
| data[].skip_reason | "quota_exceeded" | "ai_capped" | "bot_disabled" | "outside_service_window" | "opted_out" | "connection_not_active" | "concurrency_limit" | "duplicate" | … | null | required | |
| data[].trigger | object | required | |
| data[].trigger.kind | string | required | |
| data[].trigger.message_id | string | null | required | |
| data[].started_at | string (date-time) | null | required | |
| data[].finished_at | string (date-time) | null | required | |
| data[].duration_ms | integer | null | required | |
| data[].steps_failed | integer | required | |
| data[].usage | object | required | |
| data[].usage.run_units | integer | required | |
| data[].usage.ai_conversation_id | string | null | required | |
| data[].usage.external_sends | integer | required | |
| data[].sent_messages | object[] | required | The WhatsApp messages this run sent, in the order sent: the first ten, each with whether WhatsApp accepted it (delivery). Empty for a test run, which sends nothing. |
| data[].sent_messages[].message_id | string | required | |
| data[].sent_messages[].type | "template" | "text" | "interactive" | "image" | "document" | "audio" | "video" | "sticker" | required | What was sent. A template names its template in template; every other kind is a message written in the automation, and its template is null. |
| data[].sent_messages[].template | object | null | required | |
| data[].sent_messages[].to | object | required | |
| data[].sent_messages[].delivery | object | required | |
| data[].result | any | null | required | |
| data[].error | any | null | required | |
| data[].failed_step | object | null | required | |
| data[].failed_step.step_id | string | null | required | |
| data[].failed_step.step_action | string | null | required | |
| data[].failed_step.error | object | null | required | |
| data[].failure | object | null | required | |
| data[].failure.reason | string | required | Why the run failed, in plain words. |
| data[].failure.detail | string | null | required | The technical text the reason was said in place of, in full, for Details. Null when the reason is all there is. |
| data[].failure.step_id | string | null | required | The step it stopped at; null when no step failed. |
| data[].failure.step_action | string | null | required | |
| data[].failure.before_first_step | boolean | required | True when the run stopped before it recorded any step. |
| data[].executor_error | object | null | required | Windmill’s own account of why it failed or cancelled this run: the step, its error and the end of its log. name and message are null when Windmill gave no reason. Null for a run Windmill did not report failed or cancelled. |
| data[].executor_error.job_id | string | required | The Windmill job the error was read from: the failed step's, or the run's own. |
| data[].executor_error.step_id | string | null | required | The flow step Windmill marked failed; null when none did. |
| data[].executor_error.step_action | string | null | required | That step’s catalogue action, when it maps to one. |
| data[].executor_error.name | string | null | required | Windmill’s error name. |
| data[].executor_error.message | string | null | required | Windmill’s error message, at most 1000 characters, with secrets removed. |
| data[].executor_error.log_tail | string | null | required | The last lines of that step’s log, at most 20 lines and 2000 characters, with secrets removed. Null when Windmill gave none. |
| data[].test | object | null | required | |
| data[].test.customer | object | null | required | |
| data[].test.trigger_type | string | null | required | |
| data[].test.payload | object | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/runs
Trigger an automation by hand
Scopes runs:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| automation_id | string | required | |
| payload | object | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "run" | required | |
| automation_id | string | required | |
| automation_name | string | required | The name its owner knows the automation by: for one installed with another, that one’s name, and a deleted automation keeps its own. So a run is named even when its automation is not in the automations list. |
| connection_id | string | null | required | |
| version_no | integer | required | |
| status | "queued" | "running" | "waiting" | "success" | "failure" | "cancelled" | "timeout" | "skipped" | … | required | |
| skip_reason | "quota_exceeded" | "ai_capped" | "bot_disabled" | "outside_service_window" | "opted_out" | "connection_not_active" | "concurrency_limit" | "duplicate" | … | null | required | |
| trigger | object | required | |
| trigger.kind | string | required | |
| trigger.message_id | string | null | required | |
| started_at | string (date-time) | null | required | |
| finished_at | string (date-time) | null | required | |
| duration_ms | integer | null | required | |
| steps_failed | integer | required | |
| usage | object | required | |
| usage.run_units | integer | required | |
| usage.ai_conversation_id | string | null | required | |
| usage.external_sends | integer | required | |
| sent_messages | object[] | required | The WhatsApp messages this run sent, in the order sent: the first ten, each with whether WhatsApp accepted it (delivery). Empty for a test run, which sends nothing. |
| sent_messages[].message_id | string | required | |
| sent_messages[].type | "template" | "text" | "interactive" | "image" | "document" | "audio" | "video" | "sticker" | required | What was sent. A template names its template in template; every other kind is a message written in the automation, and its template is null. |
| sent_messages[].template | object | null | required | |
| sent_messages[].template.id | string | required | |
| sent_messages[].template.display_name | string | required | The name the owner gave the template, as the Templates page shows it. |
| sent_messages[].to | object | required | |
| sent_messages[].to.contact_id | string | required | |
| sent_messages[].to.name | string | null | required | The customer's name as the Inbox shows it; null when they have none. |
| sent_messages[].to.phone | string | null | required | Their number in E.164; null when they are known only by their WhatsApp id. |
| sent_messages[].delivery | object | required | |
| sent_messages[].delivery.state | "sending" | "sent" | "not_sent" | required | sent once WhatsApp accepted the message; sending while it is still on its way; not_sent when it was stopped: the customer opted out, the owner answered from the WhatsApp Business app, or WhatsApp refused it. |
| sent_messages[].delivery.reason | string | null | required | Why it was not sent, in plain words. Null unless state is not_sent. |
| sent_messages[].delivery.detail | string | null | required | WhatsApp’s or Cosend’s own text for a message that was not sent, for developers. Null otherwise. |
| result | any | null | required | |
| error | any | null | required | |
| failed_step | object | null | required | |
| failed_step.step_id | string | null | required | |
| failed_step.step_action | string | null | required | |
| failed_step.error | object | null | required | |
| failed_step.error.code | string | required | |
| failed_step.error.message | string | required | What went wrong, in plain words. |
| failed_step.error.detail | string | null | required | The technical text, in full, for Details. Null when the message is all there is, and for an error on our side. |
| failure | object | null | required | |
| failure.reason | string | required | Why the run failed, in plain words. |
| failure.detail | string | null | required | The technical text the reason was said in place of, in full, for Details. Null when the reason is all there is. |
| failure.step_id | string | null | required | The step it stopped at; null when no step failed. |
| failure.step_action | string | null | required | |
| failure.before_first_step | boolean | required | True when the run stopped before it recorded any step. |
| executor_error | object | null | required | Windmill’s own account of why it failed or cancelled this run: the step, its error and the end of its log. name and message are null when Windmill gave no reason. Null for a run Windmill did not report failed or cancelled. |
| executor_error.job_id | string | required | The Windmill job the error was read from: the failed step's, or the run's own. |
| executor_error.step_id | string | null | required | The flow step Windmill marked failed; null when none did. |
| executor_error.step_action | string | null | required | That step’s catalogue action, when it maps to one. |
| executor_error.name | string | null | required | Windmill’s error name. |
| executor_error.message | string | null | required | Windmill’s error message, at most 1000 characters, with secrets removed. |
| executor_error.log_tail | string | null | required | The last lines of that step’s log, at most 20 lines and 2000 characters, with secrets removed. Null when Windmill gave none. |
| test | object | null | required | |
| test.customer | object | null | required | |
| test.customer.name | string | required | |
| test.customer.phone | string | required | |
| test.trigger_type | string | null | required | |
| test.payload | object | required |
GET /v1/runs/{id}
One run
Scopes runs:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "run" | required | |
| automation_id | string | required | |
| automation_name | string | required | The name its owner knows the automation by: for one installed with another, that one’s name, and a deleted automation keeps its own. So a run is named even when its automation is not in the automations list. |
| connection_id | string | null | required | |
| version_no | integer | required | |
| status | "queued" | "running" | "waiting" | "success" | "failure" | "cancelled" | "timeout" | "skipped" | … | required | |
| skip_reason | "quota_exceeded" | "ai_capped" | "bot_disabled" | "outside_service_window" | "opted_out" | "connection_not_active" | "concurrency_limit" | "duplicate" | … | null | required | |
| trigger | object | required | |
| trigger.kind | string | required | |
| trigger.message_id | string | null | required | |
| started_at | string (date-time) | null | required | |
| finished_at | string (date-time) | null | required | |
| duration_ms | integer | null | required | |
| steps_failed | integer | required | |
| usage | object | required | |
| usage.run_units | integer | required | |
| usage.ai_conversation_id | string | null | required | |
| usage.external_sends | integer | required | |
| sent_messages | object[] | required | The WhatsApp messages this run sent, in the order sent: the first ten, each with whether WhatsApp accepted it (delivery). Empty for a test run, which sends nothing. |
| sent_messages[].message_id | string | required | |
| sent_messages[].type | "template" | "text" | "interactive" | "image" | "document" | "audio" | "video" | "sticker" | required | What was sent. A template names its template in template; every other kind is a message written in the automation, and its template is null. |
| sent_messages[].template | object | null | required | |
| sent_messages[].template.id | string | required | |
| sent_messages[].template.display_name | string | required | The name the owner gave the template, as the Templates page shows it. |
| sent_messages[].to | object | required | |
| sent_messages[].to.contact_id | string | required | |
| sent_messages[].to.name | string | null | required | The customer's name as the Inbox shows it; null when they have none. |
| sent_messages[].to.phone | string | null | required | Their number in E.164; null when they are known only by their WhatsApp id. |
| sent_messages[].delivery | object | required | |
| sent_messages[].delivery.state | "sending" | "sent" | "not_sent" | required | sent once WhatsApp accepted the message; sending while it is still on its way; not_sent when it was stopped: the customer opted out, the owner answered from the WhatsApp Business app, or WhatsApp refused it. |
| sent_messages[].delivery.reason | string | null | required | Why it was not sent, in plain words. Null unless state is not_sent. |
| sent_messages[].delivery.detail | string | null | required | WhatsApp’s or Cosend’s own text for a message that was not sent, for developers. Null otherwise. |
| result | any | null | required | |
| error | any | null | required | |
| failed_step | object | null | required | |
| failed_step.step_id | string | null | required | |
| failed_step.step_action | string | null | required | |
| failed_step.error | object | null | required | |
| failed_step.error.code | string | required | |
| failed_step.error.message | string | required | What went wrong, in plain words. |
| failed_step.error.detail | string | null | required | The technical text, in full, for Details. Null when the message is all there is, and for an error on our side. |
| failure | object | null | required | |
| failure.reason | string | required | Why the run failed, in plain words. |
| failure.detail | string | null | required | The technical text the reason was said in place of, in full, for Details. Null when the reason is all there is. |
| failure.step_id | string | null | required | The step it stopped at; null when no step failed. |
| failure.step_action | string | null | required | |
| failure.before_first_step | boolean | required | True when the run stopped before it recorded any step. |
| executor_error | object | null | required | Windmill’s own account of why it failed or cancelled this run: the step, its error and the end of its log. name and message are null when Windmill gave no reason. Null for a run Windmill did not report failed or cancelled. |
| executor_error.job_id | string | required | The Windmill job the error was read from: the failed step's, or the run's own. |
| executor_error.step_id | string | null | required | The flow step Windmill marked failed; null when none did. |
| executor_error.step_action | string | null | required | That step’s catalogue action, when it maps to one. |
| executor_error.name | string | null | required | Windmill’s error name. |
| executor_error.message | string | null | required | Windmill’s error message, at most 1000 characters, with secrets removed. |
| executor_error.log_tail | string | null | required | The last lines of that step’s log, at most 20 lines and 2000 characters, with secrets removed. Null when Windmill gave none. |
| test | object | null | required | |
| test.customer | object | null | required | |
| test.customer.name | string | required | |
| test.customer.phone | string | required | |
| test.trigger_type | string | null | required | |
| test.payload | object | required |
POST /v1/runs/{id}/cancel
Cancel a run that has not finished
Scopes runs:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "run" | required | |
| automation_id | string | required | |
| automation_name | string | required | The name its owner knows the automation by: for one installed with another, that one’s name, and a deleted automation keeps its own. So a run is named even when its automation is not in the automations list. |
| connection_id | string | null | required | |
| version_no | integer | required | |
| status | "queued" | "running" | "waiting" | "success" | "failure" | "cancelled" | "timeout" | "skipped" | … | required | |
| skip_reason | "quota_exceeded" | "ai_capped" | "bot_disabled" | "outside_service_window" | "opted_out" | "connection_not_active" | "concurrency_limit" | "duplicate" | … | null | required | |
| trigger | object | required | |
| trigger.kind | string | required | |
| trigger.message_id | string | null | required | |
| started_at | string (date-time) | null | required | |
| finished_at | string (date-time) | null | required | |
| duration_ms | integer | null | required | |
| steps_failed | integer | required | |
| usage | object | required | |
| usage.run_units | integer | required | |
| usage.ai_conversation_id | string | null | required | |
| usage.external_sends | integer | required | |
| sent_messages | object[] | required | The WhatsApp messages this run sent, in the order sent: the first ten, each with whether WhatsApp accepted it (delivery). Empty for a test run, which sends nothing. |
| sent_messages[].message_id | string | required | |
| sent_messages[].type | "template" | "text" | "interactive" | "image" | "document" | "audio" | "video" | "sticker" | required | What was sent. A template names its template in template; every other kind is a message written in the automation, and its template is null. |
| sent_messages[].template | object | null | required | |
| sent_messages[].template.id | string | required | |
| sent_messages[].template.display_name | string | required | The name the owner gave the template, as the Templates page shows it. |
| sent_messages[].to | object | required | |
| sent_messages[].to.contact_id | string | required | |
| sent_messages[].to.name | string | null | required | The customer's name as the Inbox shows it; null when they have none. |
| sent_messages[].to.phone | string | null | required | Their number in E.164; null when they are known only by their WhatsApp id. |
| sent_messages[].delivery | object | required | |
| sent_messages[].delivery.state | "sending" | "sent" | "not_sent" | required | sent once WhatsApp accepted the message; sending while it is still on its way; not_sent when it was stopped: the customer opted out, the owner answered from the WhatsApp Business app, or WhatsApp refused it. |
| sent_messages[].delivery.reason | string | null | required | Why it was not sent, in plain words. Null unless state is not_sent. |
| sent_messages[].delivery.detail | string | null | required | WhatsApp’s or Cosend’s own text for a message that was not sent, for developers. Null otherwise. |
| result | any | null | required | |
| error | any | null | required | |
| failed_step | object | null | required | |
| failed_step.step_id | string | null | required | |
| failed_step.step_action | string | null | required | |
| failed_step.error | object | null | required | |
| failed_step.error.code | string | required | |
| failed_step.error.message | string | required | What went wrong, in plain words. |
| failed_step.error.detail | string | null | required | The technical text, in full, for Details. Null when the message is all there is, and for an error on our side. |
| failure | object | null | required | |
| failure.reason | string | required | Why the run failed, in plain words. |
| failure.detail | string | null | required | The technical text the reason was said in place of, in full, for Details. Null when the reason is all there is. |
| failure.step_id | string | null | required | The step it stopped at; null when no step failed. |
| failure.step_action | string | null | required | |
| failure.before_first_step | boolean | required | True when the run stopped before it recorded any step. |
| executor_error | object | null | required | Windmill’s own account of why it failed or cancelled this run: the step, its error and the end of its log. name and message are null when Windmill gave no reason. Null for a run Windmill did not report failed or cancelled. |
| executor_error.job_id | string | required | The Windmill job the error was read from: the failed step's, or the run's own. |
| executor_error.step_id | string | null | required | The flow step Windmill marked failed; null when none did. |
| executor_error.step_action | string | null | required | That step’s catalogue action, when it maps to one. |
| executor_error.name | string | null | required | Windmill’s error name. |
| executor_error.message | string | null | required | Windmill’s error message, at most 1000 characters, with secrets removed. |
| executor_error.log_tail | string | null | required | The last lines of that step’s log, at most 20 lines and 2000 characters, with secrets removed. Null when Windmill gave none. |
| test | object | null | required | |
| test.customer | object | null | required | |
| test.customer.name | string | required | |
| test.customer.phone | string | required | |
| test.trigger_type | string | null | required | |
| test.payload | object | required |
GET /v1/runs/{id}/events
The per-step trace behind a run’s timeline
Scopes runs:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].seq | integer | required | |
| data[].step_id | string | null | required | |
| data[].step_action | string | null | required | |
| data[].kind | string | required | |
| data[].status | string | null | required | |
| data[].error | object | null | required | |
| data[].error.code | string | required | |
| data[].error.message | string | required | What went wrong, in plain words. |
| data[].error.detail | string | null | required | The technical text, in full, for Details. Null when the message is all there is, and for an error on our side. |
| data[].would | object | null | required | |
| data[].decision | object | null | required | |
| data[].decision.branch | string | null | required | The branch it took, by its label (otherwise when none held); null when it chose none. |
| data[].decision.why | string | required | Why, in plain words. |
| data[].test_step | object | null | required | |
| data[].test_step.outcome | "read_for_real" | "ran" | "held_back" | "sample" | required | |
| data[].test_step.says | string | required | “Read for real.”, “Ran as a real run would.”, “Held back: this step would write.”, “Held back: this read is not marked as one a Test may run.” or “Read the Test’s sample.” |
| data[].test_step.why | string | required | Why, in plain words. |
| data[].duration_ms | integer | null | required | |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
/v1/schedules
GET /v1/schedules
List schedules
Scopes notifications:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "notify_schedule" | required | |
| data[].connection_id | string | required | |
| data[].kind | string | required | |
| data[].name | string | null | required | |
| data[].to | string | null | required | |
| data[].anchor_at | string (date-time) | null | required | |
| data[].offsets_minutes | integer[] | null | required | |
| data[].timezone | string | required | |
| data[].state | string | required | |
| data[].block_reason | string | null | required | |
| data[].reference | any | null | required | |
| data[].created_at | string (date-time) | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/schedules
Create or update an anchored reminder ladder
Scopes notifications:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | required |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| kind | "anchored" | "recurring" | required | |
| name | string | null | optional | |
| to | string | required | |
| anchor_at | string (date-time) | null | optional | |
| offsets_minutes | integer[] | null | optional | |
| rrule | string | optional | |
| timezone | string | optional | |
| dst_policy | "shift_forward" | "shift_back" | "skip" | "first" | "last" | optional | |
| template_by_offset | object | optional | |
| params | object | optional | |
| priority | integer | optional | |
| quiet_hours | object | optional | |
| quiet_hours.start | string | required | |
| quiet_hours.end | string | required | |
| quiet_hours.policy | "shift_to_end" | "shift_to_start" | "skip" | "ignore" | "defer_unless_urgent" | optional | |
| skip_if_late_minutes | integer | optional | |
| expires_policy | string | optional | |
| reference | object | null | optional | |
| reference.type | string | required | |
| reference.id | string | required |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notify_schedule" | required | |
| connection_id | string | required | |
| kind | string | required | |
| name | string | null | required | |
| to | string | null | required | |
| anchor_at | string (date-time) | null | required | |
| offsets_minutes | integer[] | null | required | |
| timezone | string | required | |
| state | string | required | |
| block_reason | string | null | required | |
| reference | any | null | required | |
| occurrences | object[] | required | |
| occurrences[].id | string | required | |
| occurrences[].object | "notify_occurrence" | required | |
| occurrences[].offset_minutes | integer | null | required | |
| occurrences[].fire_at | string (date-time) | required | |
| occurrences[].state | string | required | |
| occurrences[].dedup_key | string | null | required | |
| occurrences[].expires_at | string (date-time) | null | required | |
| occurrences[].notification_id | string | null | required | |
| warnings | string[] | required | |
| created_at | string (date-time) | required |
GET /v1/schedules/{id}
One schedule and its occurrences
Scopes notifications:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notify_schedule" | required | |
| connection_id | string | required | |
| kind | string | required | |
| name | string | null | required | |
| to | string | null | required | |
| anchor_at | string (date-time) | null | required | |
| offsets_minutes | integer[] | null | required | |
| timezone | string | required | |
| state | string | required | |
| block_reason | string | null | required | |
| reference | any | null | required | |
| occurrences | object[] | required | |
| occurrences[].id | string | required | |
| occurrences[].object | "notify_occurrence" | required | |
| occurrences[].offset_minutes | integer | null | required | |
| occurrences[].fire_at | string (date-time) | required | |
| occurrences[].state | string | required | |
| occurrences[].dedup_key | string | null | required | |
| occurrences[].expires_at | string (date-time) | null | required | |
| occurrences[].notification_id | string | null | required | |
| warnings | string[] | required | |
| created_at | string (date-time) | required |
PATCH /v1/schedules/{id}
Move the anchor and re-materialise pending occurrences
Scopes notifications:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | null | optional | |
| anchor_at | string (date-time) | optional | |
| offsets_minutes | integer[] | optional | |
| timezone | string | optional | |
| dst_policy | "shift_forward" | "shift_back" | "skip" | "first" | "last" | optional | |
| template_by_offset | object | optional | |
| params | object | optional | |
| priority | integer | optional | |
| quiet_hours | object | optional | |
| quiet_hours.start | string | required | |
| quiet_hours.end | string | required | |
| quiet_hours.policy | "shift_to_end" | "shift_to_start" | "skip" | "ignore" | "defer_unless_urgent" | optional | |
| skip_if_late_minutes | integer | null | optional | |
| expires_policy | string | null | optional | |
| state | "active" | "paused" | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notify_schedule" | required | |
| connection_id | string | required | |
| kind | string | required | |
| name | string | null | required | |
| to | string | null | required | |
| anchor_at | string (date-time) | null | required | |
| offsets_minutes | integer[] | null | required | |
| timezone | string | required | |
| state | string | required | |
| block_reason | string | null | required | |
| reference | any | null | required | |
| occurrences | object[] | required | |
| occurrences[].id | string | required | |
| occurrences[].object | "notify_occurrence" | required | |
| occurrences[].offset_minutes | integer | null | required | |
| occurrences[].fire_at | string (date-time) | required | |
| occurrences[].state | string | required | |
| occurrences[].dedup_key | string | null | required | |
| occurrences[].expires_at | string (date-time) | null | required | |
| occurrences[].notification_id | string | null | required | |
| warnings | string[] | required | |
| created_at | string (date-time) | required |
DELETE /v1/schedules/{id}
Cancel a schedule, its pending occurrences and its live notifications
Scopes notifications:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "notify_schedule" | required | |
| state | "cancelled" | required | |
| occurrences_cancelled | integer | required | |
| notifications_cancelled | integer | required | |
| already_sent | string[] | required |
GET /v1/schedules/{id}/occurrences
The resolved firing times
Scopes notifications:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "notify_occurrence" | required | |
| data[].offset_minutes | integer | null | required | |
| data[].fire_at | string (date-time) | required | |
| data[].state | string | required | |
| data[].dedup_key | string | null | required | |
| data[].expires_at | string (date-time) | null | required | |
| data[].notification_id | string | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
/v1/setup
GET /v1/setup
The setup guide: each step done, to do or skipped, from the workspace’s own facts
Scopes settings:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| steps | object[] | required | |
| steps[].key | "account" | "connect" | "first_message" | "invite" | "first_automation" | required | |
| steps[].state | "done" | "todo" | "skipped" | required | |
| steps[].done_at | string (date-time) | null | required | |
| hidden | boolean | required | |
| n_done | integer | required | |
| n_total | integer | required |
PATCH /v1/setup
Hide or show the setup guide, or skip steps. Owners and admins
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| hidden | boolean | optional | |
| skipped | ("connect" | "first_message" | "invite" | "first_automation")[] | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| steps | object[] | required | |
| steps[].key | "account" | "connect" | "first_message" | "invite" | "first_automation" | required | |
| steps[].state | "done" | "todo" | "skipped" | required | |
| steps[].done_at | string (date-time) | null | required | |
| hidden | boolean | required | |
| n_done | integer | required | |
| n_total | integer | required |
/v1/setup-links
POST /v1/setup-links
A 30-minute link to a page where the person who asked connects a number, signs in to integrations, upgrades or adds a card
Scopes none; needs a credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| kind | "connect_number" | required | |
| connectors | string[] | required | The integrations the person signs in to, by key from GET /v1/connectors. The link completes when every one of them has an account. |
| plan | string | required | The plan to buy, by code from GET /v1/billing/plans. Only for a workspace with no paid subscription yet, and only up. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "setup_link" | required | |
| kind | "connect_number" | "connect_integrations" | "upgrade" | "payment_method" | required | |
| status | "open" | "completed" | "expired" | "cancelled" | required | open until the step is done (completed), 30 minutes pass (expired) or it is cancelled or replaced by a newer link of the same kind (cancelled). |
| url | string | required | The page to send the person to. Its address carries the workspace id (?org=) and no credential: it opens only for the person who asked for the link, signed in to Cosend. |
| connectors | string[] | required | |
| plan | string | null | required | |
| result | object | null | required | What the step set up: the number (connect_number), the accounts (connect_integrations, filled in one by one while open), or the plan and subscription status (upgrade, payment_method). An account connected from the page is on the workspace’s only number, or on no number when it has several; add one with POST /v1/connector-accounts/{id}/numbers. null until something is set up. |
| result.connection_id | string | null | required | |
| result.phone_number | string | null | required | |
| result.connector_accounts | object[] | required | |
| result.connector_accounts[].connector | string | required | |
| result.connector_accounts[].account_id | string | required | |
| result.plan | string | null | required | |
| result.subscription_status | string | null | required | |
| expires_at | string (date-time) | required | |
| completed_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
GET /v1/setup-links/{id}
A set-up link: open, completed (with what was set up), expired or cancelled. Only for the person who asked for it
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "setup_link" | required | |
| kind | "connect_number" | "connect_integrations" | "upgrade" | "payment_method" | required | |
| status | "open" | "completed" | "expired" | "cancelled" | required | open until the step is done (completed), 30 minutes pass (expired) or it is cancelled or replaced by a newer link of the same kind (cancelled). |
| url | string | required | The page to send the person to. Its address carries the workspace id (?org=) and no credential: it opens only for the person who asked for the link, signed in to Cosend. |
| connectors | string[] | required | |
| plan | string | null | required | |
| result | object | null | required | What the step set up: the number (connect_number), the accounts (connect_integrations, filled in one by one while open), or the plan and subscription status (upgrade, payment_method). An account connected from the page is on the workspace’s only number, or on no number when it has several; add one with POST /v1/connector-accounts/{id}/numbers. null until something is set up. |
| result.connection_id | string | null | required | |
| result.phone_number | string | null | required | |
| result.connector_accounts | object[] | required | |
| result.connector_accounts[].connector | string | required | |
| result.connector_accounts[].account_id | string | required | |
| result.plan | string | null | required | |
| result.subscription_status | string | null | required | |
| expires_at | string (date-time) | required | |
| completed_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
DELETE /v1/setup-links/{id}
Cancel an open set-up link. Only for the person who asked for it
Scopes none; needs a credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "setup_link" | required | |
| kind | "connect_number" | "connect_integrations" | "upgrade" | "payment_method" | required | |
| status | "open" | "completed" | "expired" | "cancelled" | required | open until the step is done (completed), 30 minutes pass (expired) or it is cancelled or replaced by a newer link of the same kind (cancelled). |
| url | string | required | The page to send the person to. Its address carries the workspace id (?org=) and no credential: it opens only for the person who asked for the link, signed in to Cosend. |
| connectors | string[] | required | |
| plan | string | null | required | |
| result | object | null | required | What the step set up: the number (connect_number), the accounts (connect_integrations, filled in one by one while open), or the plan and subscription status (upgrade, payment_method). An account connected from the page is on the workspace’s only number, or on no number when it has several; add one with POST /v1/connector-accounts/{id}/numbers. null until something is set up. |
| result.connection_id | string | null | required | |
| result.phone_number | string | null | required | |
| result.connector_accounts | object[] | required | |
| result.connector_accounts[].connector | string | required | |
| result.connector_accounts[].account_id | string | required | |
| result.plan | string | null | required | |
| result.subscription_status | string | null | required | |
| expires_at | string (date-time) | required | |
| completed_at | string (date-time) | null | required | |
| created_at | string (date-time) | required |
/v1/stats
GET /v1/stats/today
Today in the workspace’s time zone: replies by who sent them, and chats handed over, marked Done and opened
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | string[] | optional | WhatsApp numbers (connection ids), repeatable: the rows for any of them. Automations: those on it, and those made before numbers were required, which have none and run on every number. Runs: those that ran on it, and those that name no number of their own whose automation is one of these. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| replies_total | integer | required | |
| replies_by | object | required | |
| replies_by.assistant | integer | required | |
| replies_by.people | integer | required | |
| replies_by.phone | integer | required | |
| replies_by.automations | integer | required | |
| handed_over | integer | required | |
| resolved | integer | required | |
| new_chats | integer | required | |
| tz | string | required | |
| since | string (date-time) | required |
GET /v1/stats/week
The last 7 days, today and the 6 before it in the workspace’s time zone: replies and new chats a day
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | string[] | optional | WhatsApp numbers (connection ids), repeatable: the rows for any of them. Automations: those on it, and those made before numbers were required, which have none and run on every number. Runs: those that ran on it, and those that name no number of their own whose automation is one of these. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| tz | string | required | |
| days | object[] | required | |
| days[].date | string (date) | required | |
| days[].replies_assistant | integer | required | |
| days[].replies_people | integer | required | |
| days[].replies_phone | integer | required | |
| days[].new_chats | integer | required |
/v1/stream
GET /v1/stream
Server-Sent Events for the inbox
Scopes messages:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| connection_id | query | string | string[] | optional | |
| events | query | string | optional |
Response 200
An open Server-Sent Events stream, not a single body: id:/event:/data: frames terminated by a blank line, : keepalive comments, resumable with Last-Event-ID, and a clean close after one hour. text/event-stream
/v1/support-reports
POST /v1/support-reports
File a bug report. The payload is an allowlist: message content is refused, not stripped
Scopes none; needs a credential
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "support_report" | required | |
| request_id | string | required | |
| created_at | string (date-time) | required |
/v1/templates
GET /v1/templates
The org’s templates, filterable by status, language, category, WABA and connection
Scopes templates:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| status | query | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | optional | |
| language | query | string | optional | |
| category | query | "marketing" | "utility" | "authentication" | optional | |
| waba_id | query | string | string[] | optional | A WhatsApp Business Account, as Cosend's wab_ id (a connection's waba_id, not its meta_waba_id): only its templates. Repeat it for several accounts. |
| connection_id | query | string | string[] | optional | A connection's id: only the templates of that connection's WABA, which every number on the WABA shares. Repeat it for several numbers; numbers on one WABA give its templates once. A connection this organization does not have is connection_not_found. Given with waba_id, a template must match both. |
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].waba_id | string | required | The WhatsApp Business Account the template belongs to, as Cosend's wab_ id: the waba_id a connection carries (Meta's id is the connection's meta_waba_id) and GET /v1/templates?waba_id= takes. Every number on that account can send it. |
| data[].name | string | required | The system name: Meta's name for the template (lowercase letters, digits and underscores), and the name sends, automations and "name:language" lookups use. Generated from display_name when the template is created, and never changed after. |
| data[].display_name | string | required | The human name, as typed when the template was created (e.g. "Order shipped"). A template created before display names existed, or first seen by a sync, carries its system name here. |
| data[].language | string | required | |
| data[].category | "marketing" | "utility" | "authentication" | required | |
| data[].status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required | |
| data[].variables | string[] | required | |
| data[].sendable | boolean | required | |
| data[].paused_until | string | null | required | |
| data[].pause_level | string | null | required | |
| data[].rejected_reason | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | null | required | |
| data[].rejection | object | null | required | |
| data[].rejection.code | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | required | |
| data[].rejection.title | string | required | |
| data[].rejection.fix | string | required | |
| data[].rejection.detail | string | null | required | |
| data[].rejection.recommendation | string | null | required | |
| data[].rejection.review_url | string | required | |
| data[].edit | object | required | |
| data[].edit.allowed | boolean | required | |
| data[].edit.reason | string | null | required | Why the template cannot be edited now, in words: Meta edits approved, rejected and paused templates only, and an approved one once in 24 hours and 10 times in 30 days. null when it can. |
| data[].edit.available_at | string (date-time) | null | required | When the edit limit on this approved template next allows an edit. Counts the edits made through Cosend; null when the limit is not what stops it. |
| data[].edit.category_allowed | boolean | required | Whether an edit may change the category. Meta refuses it on an approved template. |
| data[].quality_score | "green" | "yellow" | "red" | "na" | null | required | |
| data[].components | any | required | |
| data[].authentication | object | null | required | An authentication template's security note, expiry and one-time-password button, in the shape create and edit take. Meta writes the body: "{{1}} is your verification code." null for a template with no one-time-password button. |
| data[].authentication.add_security_recommendation | boolean | required | |
| data[].authentication.code_expiration_minutes | integer | null | required | |
| data[].authentication.delivery | object | required | |
| data[].authentication.message_send_ttl_seconds | integer | null | required | The time-to-live Cosend set: 30 to 900 seconds, or -1 for 30 days. null: WhatsApp's default, or not known for a template made outside Cosend. |
| has_more | boolean | required | |
| next_cursor | string | null | required |
Errors connection_not_found
POST /v1/templates
Create a template at Meta and mirror it locally
Scopes templates:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required | |
| display_name | string | optional | The human name, free text (e.g. "Order shipped"). When name is left out, the system name is generated from it: lowercase, accents dropped, every other run of characters an underscore, at most 512 characters, with _2, _3… added when a template on the WABA already has that name. |
| name | string | optional | The system name, used as given. Optional: leave it out and it is generated from display_name. Give it to add a language to an existing template. One of name and display_name is required. |
| language | string | optional | The language, e.g. "en_US". Required on every category but authentication, which takes authentication.languages instead. |
| category | "marketing" | "utility" | "authentication" | required | |
| parameter_format | "positional" | "named" | optional | |
| components | object[] | optional | Meta's components; required on every category but authentication, which takes authentication instead. A HEADER carries its format (TEXT, IMAGE, VIDEO, DOCUMENT or LOCATION), and a header without one is sent as TEXT. An IMAGE, VIDEO or DOCUMENT header's sample is a file uploaded with POST /v1/media, named by its id in Meta's own field: {"type": "HEADER", "format": "IMAGE", "example": {"header_handle": ["med_…"]}}. The file must be a JPEG or PNG image of at most 5 MB, an MP4 video of at most 16 MB, or a PDF of at most 100 MB (WhatsApp's limits), and Cosend uploads it to Meta and sends Meta's handle in its place; a handle from Meta itself is sent as it is. A LOCATION header takes no sample, and only a utility or marketing template can have one. |
| components[].type | "BUTTONS" | required | |
| components[].buttons | object[] | required | |
| components[].buttons[].type | "QUICK_REPLY" | required | |
| components[].buttons[].text | string | required | |
| components[].buttons[].url | string | required | |
| components[].buttons[].example | string[] | optional | |
| components[].buttons[].phone_number | string | required | |
| authentication | object | null | optional | An authentication template, in Meta's shape, in place of components and language: {"languages": ["fr", "en_US"], "add_security_recommendation": true, "code_expiration_minutes": 10, "delivery": {"type": "copy_code"}, "message_send_ttl_seconds": 600}. WhatsApp writes the message and the button label in each language ("{{1}} is your verification code." in English); there is no text to set. One template is created per language, each its own resource; the answer is the version in the first language. Required when category is authentication, and refused on any other category; null is the same as leaving it out. |
| authentication.languages | string[] | required | The language versions to create, e.g. ["fr", "en_US"]: one template per language, each written by WhatsApp in its language. On an edit, left out or exactly the template's own language. |
| authentication.add_security_recommendation | boolean | required | Whether Meta adds "For your security, do not share this code." to the body. |
| authentication.code_expiration_minutes | integer | null | optional | Minutes the code stays valid, 1 to 90: Meta's footer "This code expires in N minutes." Left out or null, there is no footer. |
| authentication.delivery | object | required | How the code reaches the person: {"type": "copy_code"}, or {"type": "one_tap" | "zero_tap", "apps": [{"package_name": "com.example.app", "signature_hash": "K8a/AINcGX7"}]} (1 to 5 apps); zero-tap also takes "zero_tap_terms_accepted": true. |
| authentication.delivery.type | "copy_code" | required | |
| authentication.delivery.apps | object[] | required | The Android apps the code is handed to: 1 to 5, Meta's supported_apps. |
| authentication.delivery.zero_tap_terms_accepted | true | required | Meta's zero_tap_terms_accepted: "Set to true to indicate that you understand that your use of zero-tap authentication is subject to the WhatsApp Business Terms of Service." Sent to Meta and recorded in the audit log. |
| authentication.message_send_ttl_seconds | integer | null | optional | How long WhatsApp keeps trying to deliver the message: 30 to 900 seconds, or -1 for 30 days. Left out or null, WhatsApp's default for authentication, 10 minutes. |
| authentication.button | any | optional | Refused. WhatsApp writes the button label in each language; there is no text to set. Give delivery instead: {"type": "copy_code"}. |
| example_values | object | optional | A sample value for every variable in the TEXT header and the body, keyed per component by the name between the braces: {"body": {"1": "Pablo", "2": "860198-230332"}} or {"body": {"first_name": "Pablo"}}. Required for each such variable, because Meta rejects a template without samples; a missing one is refused with invalid_request, param example_values. Sent to Meta as each component's example. A component that already carries Meta's own example and has no entry here is sent as it is. |
| example_values.header | object | optional | |
| example_values.body | object | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| waba_id | string | required | The WhatsApp Business Account the template belongs to, as Cosend's wab_ id: the waba_id a connection carries (Meta's id is the connection's meta_waba_id) and GET /v1/templates?waba_id= takes. Every number on that account can send it. |
| name | string | required | The system name: Meta's name for the template (lowercase letters, digits and underscores), and the name sends, automations and "name:language" lookups use. Generated from display_name when the template is created, and never changed after. |
| display_name | string | required | The human name, as typed when the template was created (e.g. "Order shipped"). A template created before display names existed, or first seen by a sync, carries its system name here. |
| language | string | required | |
| category | "marketing" | "utility" | "authentication" | required | |
| status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required | |
| variables | string[] | required | |
| sendable | boolean | required | |
| paused_until | string | null | required | |
| pause_level | string | null | required | |
| rejected_reason | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | null | required | |
| rejection | object | null | required | |
| rejection.code | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | required | |
| rejection.title | string | required | |
| rejection.fix | string | required | |
| rejection.detail | string | null | required | |
| rejection.recommendation | string | null | required | |
| rejection.review_url | string | required | |
| edit | object | required | |
| edit.allowed | boolean | required | |
| edit.reason | string | null | required | Why the template cannot be edited now, in words: Meta edits approved, rejected and paused templates only, and an approved one once in 24 hours and 10 times in 30 days. null when it can. |
| edit.available_at | string (date-time) | null | required | When the edit limit on this approved template next allows an edit. Counts the edits made through Cosend; null when the limit is not what stops it. |
| edit.category_allowed | boolean | required | Whether an edit may change the category. Meta refuses it on an approved template. |
| quality_score | "green" | "yellow" | "red" | "na" | null | required | |
| components | any | required | |
| authentication | object | null | required | An authentication template's security note, expiry and one-time-password button, in the shape create and edit take. Meta writes the body: "{{1}} is your verification code." null for a template with no one-time-password button. |
| authentication.add_security_recommendation | boolean | required | |
| authentication.code_expiration_minutes | integer | null | required | |
| authentication.delivery | object | required | |
| authentication.delivery.type | "copy_code" | "one_tap" | "zero_tap" | required | Meta's otp_type. |
| authentication.delivery.apps | object[] | optional | One-tap and zero-tap: the Android apps the code is handed to (Meta's supported_apps), when the template carries them. |
| authentication.message_send_ttl_seconds | integer | null | required | The time-to-live Cosend set: 30 to 900 seconds, or -1 for 30 days. null: WhatsApp's default, or not known for a template made outside Cosend. |
Errors invalid_request
GET /v1/templates/{id}
One template. Accepts a tpl_ id or name:language
Scopes templates:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| waba_id | string | required | The WhatsApp Business Account the template belongs to, as Cosend's wab_ id: the waba_id a connection carries (Meta's id is the connection's meta_waba_id) and GET /v1/templates?waba_id= takes. Every number on that account can send it. |
| name | string | required | The system name: Meta's name for the template (lowercase letters, digits and underscores), and the name sends, automations and "name:language" lookups use. Generated from display_name when the template is created, and never changed after. |
| display_name | string | required | The human name, as typed when the template was created (e.g. "Order shipped"). A template created before display names existed, or first seen by a sync, carries its system name here. |
| language | string | required | |
| category | "marketing" | "utility" | "authentication" | required | |
| status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required | |
| variables | string[] | required | |
| sendable | boolean | required | |
| paused_until | string | null | required | |
| pause_level | string | null | required | |
| rejected_reason | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | null | required | |
| rejection | object | null | required | |
| rejection.code | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | required | |
| rejection.title | string | required | |
| rejection.fix | string | required | |
| rejection.detail | string | null | required | |
| rejection.recommendation | string | null | required | |
| rejection.review_url | string | required | |
| edit | object | required | |
| edit.allowed | boolean | required | |
| edit.reason | string | null | required | Why the template cannot be edited now, in words: Meta edits approved, rejected and paused templates only, and an approved one once in 24 hours and 10 times in 30 days. null when it can. |
| edit.available_at | string (date-time) | null | required | When the edit limit on this approved template next allows an edit. Counts the edits made through Cosend; null when the limit is not what stops it. |
| edit.category_allowed | boolean | required | Whether an edit may change the category. Meta refuses it on an approved template. |
| quality_score | "green" | "yellow" | "red" | "na" | null | required | |
| components | any | required | |
| authentication | object | null | required | An authentication template's security note, expiry and one-time-password button, in the shape create and edit take. Meta writes the body: "{{1}} is your verification code." null for a template with no one-time-password button. |
| authentication.add_security_recommendation | boolean | required | |
| authentication.code_expiration_minutes | integer | null | required | |
| authentication.delivery | object | required | |
| authentication.delivery.type | "copy_code" | "one_tap" | "zero_tap" | required | Meta's otp_type. |
| authentication.delivery.apps | object[] | optional | One-tap and zero-tap: the Android apps the code is handed to (Meta's supported_apps), when the template carries them. |
| authentication.message_send_ttl_seconds | integer | null | required | The time-to-live Cosend set: 30 to 900 seconds, or -1 for 30 days. null: WhatsApp's default, or not known for a template made outside Cosend. |
PATCH /v1/templates/{id}
Edit an approved, rejected or paused template at Meta; all its components are replaced and it goes back to review
Scopes templates:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | The template's system name. It never changes: a different one is refused. |
| language | string | optional | The template's language. It never changes: a different one is refused. |
| category | "marketing" | "utility" | "authentication" | optional | A new category, sent to Meta when it differs. Meta refuses a change on an approved template, so that is refused here first. |
| parameter_format | "positional" | "named" | optional | |
| components | object[] | optional | Meta's components, all of them; required on every category but authentication, which takes authentication instead. |
| components[].type | "BUTTONS" | required | |
| components[].buttons | object[] | required | |
| components[].buttons[].type | "QUICK_REPLY" | required | |
| components[].buttons[].text | string | required | |
| components[].buttons[].url | string | required | |
| components[].buttons[].example | string[] | optional | |
| components[].buttons[].phone_number | string | required | |
| authentication | object | null | optional | An authentication template's choices, as create takes them. An edit is one language version: languages is left out, or exactly the template's own language. |
| authentication.languages | string[] | optional | The language versions to create, e.g. ["fr", "en_US"]: one template per language, each written by WhatsApp in its language. On an edit, left out or exactly the template's own language. |
| authentication.add_security_recommendation | boolean | required | Whether Meta adds "For your security, do not share this code." to the body. |
| authentication.code_expiration_minutes | integer | null | optional | Minutes the code stays valid, 1 to 90: Meta's footer "This code expires in N minutes." Left out or null, there is no footer. |
| authentication.delivery | object | required | How the code reaches the person: {"type": "copy_code"}, or {"type": "one_tap" | "zero_tap", "apps": [{"package_name": "com.example.app", "signature_hash": "K8a/AINcGX7"}]} (1 to 5 apps); zero-tap also takes "zero_tap_terms_accepted": true. |
| authentication.delivery.type | "copy_code" | required | |
| authentication.delivery.apps | object[] | required | The Android apps the code is handed to: 1 to 5, Meta's supported_apps. |
| authentication.delivery.zero_tap_terms_accepted | true | required | Meta's zero_tap_terms_accepted: "Set to true to indicate that you understand that your use of zero-tap authentication is subject to the WhatsApp Business Terms of Service." Sent to Meta and recorded in the audit log. |
| authentication.message_send_ttl_seconds | integer | null | optional | How long WhatsApp keeps trying to deliver the message: 30 to 900 seconds, or -1 for 30 days. Left out or null, WhatsApp's default for authentication, 10 minutes. |
| authentication.button | any | optional | Refused. WhatsApp writes the button label in each language; there is no text to set. Give delivery instead: {"type": "copy_code"}. |
| example_values | object | optional | A sample value for every variable in the TEXT header and the body, keyed per component by the name between the braces: {"body": {"1": "Pablo", "2": "860198-230332"}} or {"body": {"first_name": "Pablo"}}. Required for each such variable, because Meta rejects a template without samples; a missing one is refused with invalid_request, param example_values. Sent to Meta as each component's example. A component that already carries Meta's own example and has no entry here is sent as it is. |
| example_values.header | object | optional | |
| example_values.body | object | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| waba_id | string | required | The WhatsApp Business Account the template belongs to, as Cosend's wab_ id: the waba_id a connection carries (Meta's id is the connection's meta_waba_id) and GET /v1/templates?waba_id= takes. Every number on that account can send it. |
| name | string | required | The system name: Meta's name for the template (lowercase letters, digits and underscores), and the name sends, automations and "name:language" lookups use. Generated from display_name when the template is created, and never changed after. |
| display_name | string | required | The human name, as typed when the template was created (e.g. "Order shipped"). A template created before display names existed, or first seen by a sync, carries its system name here. |
| language | string | required | |
| category | "marketing" | "utility" | "authentication" | required | |
| status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required | |
| variables | string[] | required | |
| sendable | boolean | required | |
| paused_until | string | null | required | |
| pause_level | string | null | required | |
| rejected_reason | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | null | required | |
| rejection | object | null | required | |
| rejection.code | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | required | |
| rejection.title | string | required | |
| rejection.fix | string | required | |
| rejection.detail | string | null | required | |
| rejection.recommendation | string | null | required | |
| rejection.review_url | string | required | |
| edit | object | required | |
| edit.allowed | boolean | required | |
| edit.reason | string | null | required | Why the template cannot be edited now, in words: Meta edits approved, rejected and paused templates only, and an approved one once in 24 hours and 10 times in 30 days. null when it can. |
| edit.available_at | string (date-time) | null | required | When the edit limit on this approved template next allows an edit. Counts the edits made through Cosend; null when the limit is not what stops it. |
| edit.category_allowed | boolean | required | Whether an edit may change the category. Meta refuses it on an approved template. |
| quality_score | "green" | "yellow" | "red" | "na" | null | required | |
| components | any | required | |
| authentication | object | null | required | An authentication template's security note, expiry and one-time-password button, in the shape create and edit take. Meta writes the body: "{{1}} is your verification code." null for a template with no one-time-password button. |
| authentication.add_security_recommendation | boolean | required | |
| authentication.code_expiration_minutes | integer | null | required | |
| authentication.delivery | object | required | |
| authentication.delivery.type | "copy_code" | "one_tap" | "zero_tap" | required | Meta's otp_type. |
| authentication.delivery.apps | object[] | optional | One-tap and zero-tap: the Android apps the code is handed to (Meta's supported_apps), when the template carries them. |
| authentication.message_send_ttl_seconds | integer | null | required | The time-to-live Cosend set: 30 to 900 seconds, or -1 for 30 days. null: WhatsApp's default, or not known for a template made outside Cosend. |
Errors invalid_request
DELETE /v1/templates/{id}
Delete it at Meta; it leaves Cosend only once Meta confirms, and a refusal is Meta’s words
Scopes templates:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "template" | required | |
| deleted | true | required | |
| status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required |
POST /v1/templates/{id}/copy
Copy one language version to another WhatsApp account of the same business; Meta reviews it there again
Scopes templates:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| waba_id | string | required | The WhatsApp Business Account to copy to, as Cosend's wab_ id (a connection's waba_id, not its meta_waba_id). It must be another account of the same business portfolio. |
| display_name | string | optional | A new human name for the copy, from which its system name is made (with _2, _3… added when the account already has that name). Without it the copy keeps the template’s names, and an account that already has a template of that name in that language is 409 template_name_taken. |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| waba_id | string | required | The WhatsApp Business Account the template belongs to, as Cosend's wab_ id: the waba_id a connection carries (Meta's id is the connection's meta_waba_id) and GET /v1/templates?waba_id= takes. Every number on that account can send it. |
| name | string | required | The system name: Meta's name for the template (lowercase letters, digits and underscores), and the name sends, automations and "name:language" lookups use. Generated from display_name when the template is created, and never changed after. |
| display_name | string | required | The human name, as typed when the template was created (e.g. "Order shipped"). A template created before display names existed, or first seen by a sync, carries its system name here. |
| language | string | required | |
| category | "marketing" | "utility" | "authentication" | required | |
| status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required | |
| variables | string[] | required | |
| sendable | boolean | required | |
| paused_until | string | null | required | |
| pause_level | string | null | required | |
| rejected_reason | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | null | required | |
| rejection | object | null | required | |
| rejection.code | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | required | |
| rejection.title | string | required | |
| rejection.fix | string | required | |
| rejection.detail | string | null | required | |
| rejection.recommendation | string | null | required | |
| rejection.review_url | string | required | |
| edit | object | required | |
| edit.allowed | boolean | required | |
| edit.reason | string | null | required | Why the template cannot be edited now, in words: Meta edits approved, rejected and paused templates only, and an approved one once in 24 hours and 10 times in 30 days. null when it can. |
| edit.available_at | string (date-time) | null | required | When the edit limit on this approved template next allows an edit. Counts the edits made through Cosend; null when the limit is not what stops it. |
| edit.category_allowed | boolean | required | Whether an edit may change the category. Meta refuses it on an approved template. |
| quality_score | "green" | "yellow" | "red" | "na" | null | required | |
| components | any | required | |
| authentication | object | null | required | An authentication template's security note, expiry and one-time-password button, in the shape create and edit take. Meta writes the body: "{{1}} is your verification code." null for a template with no one-time-password button. |
| authentication.add_security_recommendation | boolean | required | |
| authentication.code_expiration_minutes | integer | null | required | |
| authentication.delivery | object | required | |
| authentication.delivery.type | "copy_code" | "one_tap" | "zero_tap" | required | Meta's otp_type. |
| authentication.delivery.apps | object[] | optional | One-tap and zero-tap: the Android apps the code is handed to (Meta's supported_apps), when the template carries them. |
| authentication.message_send_ttl_seconds | integer | null | required | The time-to-live Cosend set: 30 to 900 seconds, or -1 for 30 days. null: WhatsApp's default, or not known for a template made outside Cosend. |
Errors template_name_taken
GET /v1/templates/category-changes
The template category changes Meta made or announced that nobody has acknowledged
Scopes templates:read
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | Every category change no one in the workspace has acknowledged, newest first, on templates that are not deleted. A detected one is listed only while no later change to its template has arrived. |
| data[].id | string | required | The change, as a decimal string. POST /v1/templates/category-changes/{id}/acknowledge takes it. |
| data[].object | "template_category_change" | required | |
| data[].kind | "changed" | "scheduled" | "detected" | required | changed: Meta has moved the template to new_category. scheduled: Meta's advance notice that it will, at takes_effect_at; the template is still previous_category. detected: Meta has detected that new_category (Meta's correct_category) is the right category for the template, which is still previous_category; a later change to the template's category replaces it. |
| data[].template_id | string | required | |
| data[].name | string | required | The template's system name, Meta's. |
| data[].display_name | string | required | |
| data[].language | string | required | |
| data[].previous_category | "marketing" | "utility" | "authentication" | null | required | The category before the change. Null when Meta did not say. |
| data[].new_category | "marketing" | "utility" | "authentication" | required | |
| data[].occurred_at | string (date-time) | required | When Cosend learned of it: the webhook, the sync or the create that carried it. For a changed one, when Meta changed it. |
| data[].takes_effect_at | string (date-time) | null | required | When Meta says the category will change (Meta's category_update_timestamp). Only on a scheduled change, and null if Meta's notice gave none. Meta sends no other date with a category change, so no review deadline is published. |
| data[].acknowledged_at | string (date-time) | null | required | |
| data[].acknowledged_by | object | null | required | Who acknowledged it, for the whole workspace. Null while it is open. |
| data[].acknowledged_by.kind | string | required | As the audit trail records an actor: user for a person, api_key for a key. |
| data[].acknowledged_by.id | string | null | required | |
| data[].acknowledged_by.label | string | null | required | Their name as it was then. |
POST /v1/templates/category-changes/{id}/acknowledge
Acknowledge a template category change for the whole workspace; it leaves the open list
Scopes templates:write
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | The change, as a decimal string. POST /v1/templates/category-changes/{id}/acknowledge takes it. |
| object | "template_category_change" | required | |
| kind | "changed" | "scheduled" | "detected" | required | changed: Meta has moved the template to new_category. scheduled: Meta's advance notice that it will, at takes_effect_at; the template is still previous_category. detected: Meta has detected that new_category (Meta's correct_category) is the right category for the template, which is still previous_category; a later change to the template's category replaces it. |
| template_id | string | required | |
| name | string | required | The template's system name, Meta's. |
| display_name | string | required | |
| language | string | required | |
| previous_category | "marketing" | "utility" | "authentication" | null | required | The category before the change. Null when Meta did not say. |
| new_category | "marketing" | "utility" | "authentication" | required | |
| occurred_at | string (date-time) | required | When Cosend learned of it: the webhook, the sync or the create that carried it. For a changed one, when Meta changed it. |
| takes_effect_at | string (date-time) | null | required | When Meta says the category will change (Meta's category_update_timestamp). Only on a scheduled change, and null if Meta's notice gave none. Meta sends no other date with a category change, so no review deadline is published. |
| acknowledged_at | string (date-time) | null | required | |
| acknowledged_by | object | null | required | Who acknowledged it, for the whole workspace. Null while it is open. |
| acknowledged_by.kind | string | required | As the audit trail records an actor: user for a person, api_key for a key. |
| acknowledged_by.id | string | null | required | |
| acknowledged_by.label | string | null | required | Their name as it was then. |
POST /v1/templates/sync
Read the number’s templates from Meta now and answer with the synced list
Scopes templates:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| connection_id | string | required |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| object | "template_sync" | required | |
| waba_id | string | required | The WhatsApp Business Account that was read, as Cosend's wab_ id: the connection's waba_id, and what GET /v1/templates?waba_id= takes. |
| synced_at | string (date-time) | required | |
| templates | object[] | required | |
| templates[].id | string | required | |
| templates[].waba_id | string | required | The WhatsApp Business Account the template belongs to, as Cosend's wab_ id: the waba_id a connection carries (Meta's id is the connection's meta_waba_id) and GET /v1/templates?waba_id= takes. Every number on that account can send it. |
| templates[].name | string | required | The system name: Meta's name for the template (lowercase letters, digits and underscores), and the name sends, automations and "name:language" lookups use. Generated from display_name when the template is created, and never changed after. |
| templates[].display_name | string | required | The human name, as typed when the template was created (e.g. "Order shipped"). A template created before display names existed, or first seen by a sync, carries its system name here. |
| templates[].language | string | required | |
| templates[].category | "marketing" | "utility" | "authentication" | required | |
| templates[].status | "pending" | "approved" | "rejected" | "paused" | "disabled" | "in_appeal" | "pending_deletion" | "deleted" | … | required | |
| templates[].variables | string[] | required | |
| templates[].sendable | boolean | required | |
| templates[].paused_until | string | null | required | |
| templates[].pause_level | string | null | required | |
| templates[].rejected_reason | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | null | required | |
| templates[].rejection | object | null | required | |
| templates[].rejection.code | "invalid_format" | "incorrect_category" | "abusive_content" | "promotional" | "scam" | "tag_content_mismatch" | "category_not_available" | "none" | required | |
| templates[].rejection.title | string | required | |
| templates[].rejection.fix | string | required | |
| templates[].rejection.detail | string | null | required | |
| templates[].rejection.recommendation | string | null | required | |
| templates[].rejection.review_url | string | required | |
| templates[].edit | object | required | |
| templates[].edit.allowed | boolean | required | |
| templates[].edit.reason | string | null | required | Why the template cannot be edited now, in words: Meta edits approved, rejected and paused templates only, and an approved one once in 24 hours and 10 times in 30 days. null when it can. |
| templates[].edit.available_at | string (date-time) | null | required | When the edit limit on this approved template next allows an edit. Counts the edits made through Cosend; null when the limit is not what stops it. |
| templates[].edit.category_allowed | boolean | required | Whether an edit may change the category. Meta refuses it on an approved template. |
| templates[].quality_score | "green" | "yellow" | "red" | "na" | null | required | |
| templates[].components | any | required | |
| templates[].authentication | object | null | required | An authentication template's security note, expiry and one-time-password button, in the shape create and edit take. Meta writes the body: "{{1}} is your verification code." null for a template with no one-time-password button. |
| templates[].authentication.add_security_recommendation | boolean | required | |
| templates[].authentication.code_expiration_minutes | integer | null | required | |
| templates[].authentication.delivery | object | required | |
| templates[].authentication.message_send_ttl_seconds | integer | null | required | The time-to-live Cosend set: 30 to 900 seconds, or -1 for 30 days. null: WhatsApp's default, or not known for a template made outside Cosend. |
| skipped_count | integer | required | |
| skipped | object[] | required | |
| skipped[].name | string | required | |
| skipped[].language | string | required | |
| skipped[].reason | string | required |
/v1/tools
GET /v1/tools/message-cost
What one WhatsApp message costs in a country, from Meta’s rate cards. Free, no sign-in
Scopes none; the contract declares no credential
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| country | query | string | required | The country the messages go to, as its two-letter ISO code: "GB", "US", "DE". |
| currency | query | string | required | One of Meta's 16 rate-card currencies: USD, AED, ARS, AUD, BRL, CLP, COP, EUR, GBP, IDR, INR, MXN, MYR, PEN, SAR, SGD. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| market | string | required | The market Meta charges the country in, as its rate card names it: the country itself ("Germany"), a region ("Rest of Latin America", "North America") or "Other". |
| currency | string | required | The currency the rates are in. |
| effective_from | string | required | The day these rates took effect, YYYY-MM-DD, by the business's Messaging account timezone. |
| rates | object | required | |
| rates.marketing | number | required | The price of one delivered message. |
| rates.utility | number | required | The price of one delivered message. |
| rates.authentication | number | required | The price of one delivered message. |
| rates.authentication_international | number | optional | Only in the markets that have one. Meta's Authentication-International rates page says which businesses it applies to. |
| rates.service | number | required | The price of one delivered message. |
| notes | string[] | required | Meta's rules that change what a business pays: delivered messages only, the free service messages, free entry point windows, volume tiers. |
POST /v1/tools/template-check
Check a template against WhatsApp’s rules before Meta reviews it. Free, no sign-in
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| category | "marketing" | "utility" | "authentication" | required | |
| language | string | optional | The language, e.g. "en_US". Required on every category but authentication, which takes authentication.languages instead. |
| parameter_format | "positional" | "named" | optional | |
| components | object[] | optional | Meta's components, as POST /v1/templates takes them; required on every category but authentication, which takes authentication instead. |
| components[].type | "BUTTONS" | required | |
| components[].buttons | object[] | required | |
| components[].buttons[].type | "QUICK_REPLY" | required | |
| components[].buttons[].text | string | required | |
| components[].buttons[].url | string | required | |
| components[].buttons[].example | string[] | optional | |
| components[].buttons[].phone_number | string | required | |
| authentication | object | null | optional | An authentication template, in Meta's shape, in place of components and language: {"languages": ["fr", "en_US"], "add_security_recommendation": true, "code_expiration_minutes": 10, "delivery": {"type": "copy_code"}, "message_send_ttl_seconds": 600}. WhatsApp writes the message and the button label in each language ("{{1}} is your verification code." in English); there is no text to set. One template is created per language, each its own resource; the answer is the version in the first language. Required when category is authentication, and refused on any other category; null is the same as leaving it out. |
| authentication.languages | string[] | required | The language versions to create, e.g. ["fr", "en_US"]: one template per language, each written by WhatsApp in its language. On an edit, left out or exactly the template's own language. |
| authentication.add_security_recommendation | boolean | required | Whether Meta adds "For your security, do not share this code." to the body. |
| authentication.code_expiration_minutes | integer | null | optional | Minutes the code stays valid, 1 to 90: Meta's footer "This code expires in N minutes." Left out or null, there is no footer. |
| authentication.delivery | object | required | How the code reaches the person: {"type": "copy_code"}, or {"type": "one_tap" | "zero_tap", "apps": [{"package_name": "com.example.app", "signature_hash": "K8a/AINcGX7"}]} (1 to 5 apps); zero-tap also takes "zero_tap_terms_accepted": true. |
| authentication.delivery.type | "copy_code" | required | |
| authentication.delivery.apps | object[] | required | The Android apps the code is handed to: 1 to 5, Meta's supported_apps. |
| authentication.delivery.zero_tap_terms_accepted | true | required | Meta's zero_tap_terms_accepted: "Set to true to indicate that you understand that your use of zero-tap authentication is subject to the WhatsApp Business Terms of Service." Sent to Meta and recorded in the audit log. |
| authentication.message_send_ttl_seconds | integer | null | optional | How long WhatsApp keeps trying to deliver the message: 30 to 900 seconds, or -1 for 30 days. Left out or null, WhatsApp's default for authentication, 10 minutes. |
| authentication.button | any | optional | Refused. WhatsApp writes the button label in each language; there is no text to set. Give delivery instead: {"type": "copy_code"}. |
| example_values | object | optional | A sample value for every variable in the TEXT header and the body, keyed per component by the name between the braces: {"body": {"1": "Pablo", "2": "860198-230332"}} or {"body": {"first_name": "Pablo"}}. Required for each such variable, because Meta rejects a template without samples; a missing one is refused with invalid_request, param example_values. Sent to Meta as each component's example. A component that already carries Meta's own example and has no entry here is sent as it is. |
| example_values.header | object | optional | |
| example_values.body | object | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| verdict | "ready" | "fix_first" | "warnings" | required | ready: no problem found. fix_first: at least one error, which Meta rejects the template for. warnings: no error, but at least one warning. Meta reviews every template itself, so ready is not an approval. |
| problems | object[] | required | |
| problems[].where | string | required | Where it is, as a JSON pointer into the request, e.g. "/components/1/text". |
| problems[].rule | "buttons_empty" | "buttons_max" | "url_buttons_max" | "phone_buttons_max" | "quick_replies_grouped" | "url_variables_max" | "url_variable_at_end" | "url_example_missing" | … | required | |
| problems[].level | "error" | "warning" | required | error: breaks one of Meta's exact rules, and Meta rejects the template for it. warning: a signal Meta names in its review and categorization pages, where Meta will likely reject the template or make it marketing. |
| problems[].message | string | required | What is wrong, in plain words. |
| problems[].fix | string | required | What to change, in plain words. |
Errors invalid_request
POST /v1/tools/whatsapp-link
A WhatsApp chat link and its QR code for a number. Free, no sign-in
Scopes none; the contract declares no credential
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| country | string | required | The country the number belongs to, as its two-letter ISO code: "GB", "US", "DE". A number of another country is refused. |
| number | string | required | The WhatsApp number as people type it: as written at home (07700 900123), with + (+44 7700 900123), with 00 (0044 7700 900123) or with the country code in front (44 7700 900123). Spaces, dashes, brackets and digits in any script are fine. |
| message | string | optional | The message the chat starts with, at most 500 characters. Left out or blank, the link opens an empty chat. |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| digits | string | required | The full international number in digits, with no +: what wa.me takes. |
| link | string | required | The chat link, https://wa.me/<digits>, with ?text= and the message when there is one. |
| qr_svg | string | null | required | The link as a QR code: an SVG image, black on white, with a quiet zone of 4 modules. null when the link is too long for any QR code, and qr_note says so. |
| qr_note | string | null | required | "This message is too long for a QR code. Use the link instead." when qr_svg is null; null otherwise. |
/v1/usage
GET /v1/usage
Usage for one billing period: used is live, allowances are plan configuration
Scopes usage:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| period | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| period | object | required | |
| period.start | string | required | |
| period.end | string | required | |
| period.current | boolean | required | |
| plan | string | null | required | |
| meters | object | required | |
| meters.ai_conversation | object | required | |
| meters.ai_conversation.used | integer | required | |
| meters.ai_conversation.included | integer | null | required | |
| meters.ai_conversation.hard_cap | integer | null | required | |
| meters.ai_conversation.basis | "period" | "lifetime" | "concurrent" | null | required | |
| meters.ai_conversation.billed_by | "meta" | optional | |
| meters.ai_conversation.overage_used | integer | optional | |
| meters.ai_conversation.overage_minor | string | optional | |
| meters.automation_run | object | required | |
| meters.automation_run.used | integer | required | |
| meters.automation_run.included | integer | null | required | |
| meters.automation_run.hard_cap | integer | null | required | |
| meters.automation_run.basis | "period" | "lifetime" | "concurrent" | null | required | |
| meters.automation_run.billed_by | "meta" | optional | |
| meters.automation_run.overage_used | integer | optional | |
| meters.automation_run.overage_minor | string | optional | |
| meters.message_sent | object | required | |
| meters.message_sent.used | integer | required | |
| meters.message_sent.included | integer | null | required | |
| meters.message_sent.hard_cap | integer | null | required | |
| meters.message_sent.basis | "period" | "lifetime" | "concurrent" | null | required | |
| meters.message_sent.billed_by | "meta" | optional | |
| meters.message_sent.overage_used | integer | optional | |
| meters.message_sent.overage_minor | string | optional | |
| meters.connection_month | object | required | |
| meters.connection_month.used | integer | required | |
| meters.connection_month.included | integer | null | required | |
| meters.connection_month.hard_cap | integer | null | required | |
| meters.connection_month.basis | "period" | "lifetime" | "concurrent" | null | required | |
| meters.connection_month.billed_by | "meta" | optional | |
| meters.connection_month.overage_used | integer | optional | |
| meters.connection_month.overage_minor | string | optional | |
| meters.seat_month | object | required | |
| meters.seat_month.used | integer | required | |
| meters.seat_month.included | integer | null | required | |
| meters.seat_month.hard_cap | integer | null | required | |
| meters.seat_month.basis | "period" | "lifetime" | "concurrent" | null | required | |
| meters.seat_month.billed_by | "meta" | optional | |
| meters.seat_month.overage_used | integer | optional | |
| meters.seat_month.overage_minor | string | optional | |
| direct_message_sent | object | optional | |
| direct_message_sent.used | integer | required | |
| estimated_invoice_minor | integer | required | |
| maximum_invoice_minor | integer | required | |
| currency | string | required | |
| ai_uses | object | required | |
| ai_uses.assistant_reply | integer | required | |
| ai_uses.try_it | integer | required | |
| ai_uses.extract | integer | required | |
| ai_uses.knowledge_search | integer | required | |
| ai_uses.knowledge_reading | integer | required | |
| ai_uses.read_again | integer | required | |
| ai_uses.photo_reading | integer | required | |
| ai_uses.knowledge_summary | integer | required | |
| ai_uses.transcription | integer | required | |
| ai_uses.moderation | integer | required | |
| ai_uses.authoring | integer | required | |
| ai_uses.draft | integer | required | |
| ai_uses.translate | integer | required |
/v1/webhook-deliveries
POST /v1/webhook-deliveries/{id}/replay
Re-deliver one event. A new attempt row, the same evt_
Scopes webhooks:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| delivery | object | required | |
| delivery.id | string | required | |
| delivery.object | "webhook_delivery" | required | |
| delivery.endpoint_id | string | required | |
| delivery.event | string | required | |
| delivery.event_id | string | null | required | |
| delivery.attempt | integer | required | |
| delivery.status_code | integer | null | required | |
| delivery.duration_ms | integer | null | required | |
| delivery.error | string | null | required | |
| delivery.next_retry_at | string | null | required | |
| delivery.delivered_at | string | null | required | |
| delivery.created_at | string | required | |
| delivery.status | "delivered" | "retrying" | "failed" | "stopped" | required | |
| delivery.stopped_reason | string | null | required |
/v1/webhook-endpoints
GET /v1/webhook-endpoints
The org’s registered endpoints
Scopes webhooks:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "webhook_endpoint" | required | |
| data[].url | string | required | |
| data[].events | string[] | required | |
| data[].connection_id | string | null | required | |
| data[].filters | object | required | |
| data[].filters.message_types | ("text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | …)[] | required | |
| data[].filters.message_statuses | ("queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | …)[] | required | |
| data[].filters.labels | string[] | required | |
| data[].filters.assignee_ids | string[] | required | |
| data[].filters.automation_ids | string[] | required | |
| data[].filters.exclude_sent_by_api_key_ids | string[] | required | |
| data[].include_contact | boolean | required | |
| data[].is_enabled | boolean | required | |
| data[].api_version | string | required | |
| data[].consecutive_failures | integer | required | |
| data[].disabled_at | string | null | required | |
| data[].disabled_reason | string | null | required | |
| data[].last_success_at | string | null | required | |
| data[].last_failure_at | string | null | required | |
| data[].created_at | string | required | |
| data[].updated_at | string | required | |
| data[].secret | string | optional | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/webhook-endpoints
Register an endpoint. The signing secret is returned ONCE
Scopes webhooks:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | required | |
| events | ("message.received" | "message.sent" | "message.status" | "message.echo" | "conversation.created" | "conversation.updated" | "conversation.window_expiring" | "conversation.lock_changed" | …)[] | required | |
| connection_id | string | null | optional | |
| filters | object | optional | |
| filters.message_types | ("text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | …)[] | optional | |
| filters.message_statuses | ("queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | …)[] | optional | |
| filters.labels | string[] | optional | |
| filters.assignee_ids | string[] | optional | |
| filters.automation_ids | string[] | optional | |
| filters.exclude_sent_by_api_key_ids | string[] | optional | |
| include_contact | boolean | optional |
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "webhook_endpoint" | required | |
| url | string | required | |
| events | string[] | required | |
| connection_id | string | null | required | |
| filters | object | required | |
| filters.message_types | ("text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | …)[] | required | |
| filters.message_statuses | ("queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | …)[] | required | |
| filters.labels | string[] | required | |
| filters.assignee_ids | string[] | required | |
| filters.automation_ids | string[] | required | |
| filters.exclude_sent_by_api_key_ids | string[] | required | |
| include_contact | boolean | required | |
| is_enabled | boolean | required | |
| api_version | string | required | |
| consecutive_failures | integer | required | |
| disabled_at | string | null | required | |
| disabled_reason | string | null | required | |
| last_success_at | string | null | required | |
| last_failure_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| secret | string | optional |
PATCH /v1/webhook-endpoints/{id}
Change the URL, the subscribed events, the filters, or re-enable after an auto-disable
Scopes webhooks:write
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | optional | |
| events | ("message.received" | "message.sent" | "message.status" | "message.echo" | "conversation.created" | "conversation.updated" | "conversation.window_expiring" | "conversation.lock_changed" | …)[] | optional | |
| is_enabled | boolean | optional | |
| filters | object | optional | |
| filters.message_types | ("text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | …)[] | optional | |
| filters.message_statuses | ("queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | …)[] | optional | |
| filters.labels | string[] | optional | |
| filters.assignee_ids | string[] | optional | |
| filters.automation_ids | string[] | optional | |
| filters.exclude_sent_by_api_key_ids | string[] | optional | |
| include_contact | boolean | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "webhook_endpoint" | required | |
| url | string | required | |
| events | string[] | required | |
| connection_id | string | null | required | |
| filters | object | required | |
| filters.message_types | ("text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | …)[] | required | |
| filters.message_statuses | ("queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | …)[] | required | |
| filters.labels | string[] | required | |
| filters.assignee_ids | string[] | required | |
| filters.automation_ids | string[] | required | |
| filters.exclude_sent_by_api_key_ids | string[] | required | |
| include_contact | boolean | required | |
| is_enabled | boolean | required | |
| api_version | string | required | |
| consecutive_failures | integer | required | |
| disabled_at | string | null | required | |
| disabled_reason | string | null | required | |
| last_success_at | string | null | required | |
| last_failure_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| secret | string | optional |
DELETE /v1/webhook-endpoints/{id}
Remove an endpoint and its signing secret
Scopes webhooks:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "webhook_endpoint" | required | |
| deleted | true | required |
GET /v1/webhook-endpoints/{id}/deliveries
Attempt history with status codes
Scopes webhooks:read
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| limit | query | integer | optional | |
| after | query | string | optional | |
| before | query | string | optional |
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].object | "webhook_delivery" | required | |
| data[].endpoint_id | string | required | |
| data[].event | string | required | |
| data[].event_id | string | null | required | |
| data[].attempt | integer | required | |
| data[].status_code | integer | null | required | |
| data[].duration_ms | integer | null | required | |
| data[].error | string | null | required | |
| data[].next_retry_at | string | null | required | |
| data[].delivered_at | string | null | required | |
| data[].created_at | string | required | |
| data[].status | "delivered" | "retrying" | "failed" | "stopped" | required | |
| data[].stopped_reason | string | null | required | |
| has_more | boolean | required | |
| next_cursor | string | null | required |
POST /v1/webhook-endpoints/{id}/rotate-secret
Mint a new signing secret. Both stay valid for 24 hours
Scopes webhooks:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | |
| object | "webhook_endpoint" | required | |
| url | string | required | |
| events | string[] | required | |
| connection_id | string | null | required | |
| filters | object | required | |
| filters.message_types | ("text" | "image" | "audio" | "video" | "document" | "sticker" | "location" | "contacts" | …)[] | required | |
| filters.message_statuses | ("queued" | "sending" | "sent" | "delivered" | "read" | "played" | "failed" | "deleted" | …)[] | required | |
| filters.labels | string[] | required | |
| filters.assignee_ids | string[] | required | |
| filters.automation_ids | string[] | required | |
| filters.exclude_sent_by_api_key_ids | string[] | required | |
| include_contact | boolean | required | |
| is_enabled | boolean | required | |
| api_version | string | required | |
| consecutive_failures | integer | required | |
| disabled_at | string | null | required | |
| disabled_reason | string | null | required | |
| last_success_at | string | null | required | |
| last_failure_at | string | null | required | |
| created_at | string | required | |
| updated_at | string | required | |
| secret | string | optional |
POST /v1/webhook-endpoints/{id}/test
Send a ping event synchronously and return the result
Scopes webhooks:write
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| delivered | boolean | required | |
| delivery | object | required | |
| delivery.id | string | required | |
| delivery.object | "webhook_delivery" | required | |
| delivery.endpoint_id | string | required | |
| delivery.event | string | required | |
| delivery.event_id | string | null | required | |
| delivery.attempt | integer | required | |
| delivery.status_code | integer | null | required | |
| delivery.duration_ms | integer | null | required | |
| delivery.error | string | null | required | |
| delivery.next_retry_at | string | null | required | |
| delivery.delivered_at | string | null | required | |
| delivery.created_at | string | required | |
| delivery.status | "delivered" | "retrying" | "failed" | "stopped" | required | |
| delivery.stopped_reason | string | null | required |
/.well-known
GET /.well-known/jwks.json
The public key MCP access tokens are signed with (RFC 7517)
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| keys | object[] | required | |
| keys[].kid | string | required |
GET /.well-known/oauth-authorization-server
OAuth 2.0 Authorization Server Metadata (RFC 8414) for the MCP sign-in
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| issuer | string | required | |
| authorization_endpoint | string | required | |
| token_endpoint | string | required | |
| registration_endpoint | string | required | |
| jwks_uri | string | required | |
| scopes_supported | string[] | required | |
| response_types_supported | string[] | required | |
| response_modes_supported | string[] | required | |
| grant_types_supported | string[] | required | |
| token_endpoint_auth_methods_supported | string[] | required | |
| code_challenge_methods_supported | string[] | required | |
| authorization_response_iss_parameter_supported | boolean | required | |
| client_id_metadata_document_supported | boolean | required |
GET /.well-known/oauth-protected-resource/mcp
OAuth 2.0 Protected Resource Metadata (RFC 9728)
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| resource | string | required | |
| authorization_servers | string[] | required | |
| scopes_supported | string[] | required | |
| bearer_methods_supported | string[] | required | |
| resource_documentation | string | required |
/{version}
GET /{version}/{node}
Graph passthrough: read the phone number, the WABA, or a media object
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}
Graph passthrough: update the phone number's own settings, such as its two-step verification PIN (webhook_configuration is refused)
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
DELETE /{version}/{node}
Graph passthrough: delete a media object
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/analytics
Graph passthrough: message volume analytics
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/block_users
Graph passthrough: list blocked users
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/block_users
Graph passthrough: block or unblock users
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/conversation_analytics
Graph passthrough: conversation analytics
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/conversational_automation
Graph passthrough: read ice-breakers and commands
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/conversational_automation
Graph passthrough: set ice-breakers and commands
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/media
Graph passthrough: media upload
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/message_templates
Graph passthrough: list templates
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/message_templates
Graph passthrough: create a template
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
DELETE /{version}/{node}/message_templates
Graph passthrough: delete a template
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/messages
Graph passthrough: send, mark-read, typing, reaction
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/phone_numbers
Graph passthrough: list phone numbers on the WABA
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/pricing_analytics
Graph passthrough: per-message cost analytics
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/register
Graph passthrough: register, for cloud_api connections only (refused on coexistence)
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/request_code
Graph passthrough: request a verification code (refused on coexistence)
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/settings
Graph passthrough: read the phone number's settings
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/settings
Graph passthrough: update the phone number's settings
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/template_analytics
Graph passthrough: template analytics
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/verify_code
Graph passthrough: verify a code (refused on coexistence)
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/{node}/whatsapp_business_profile
Graph passthrough: read the business profile
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
POST /{version}/{node}/whatsapp_business_profile
Graph passthrough: update the business profile (refused on coexistence)
Scopes runtime:proxy
Response 200
Meta's Graph API response, forwarded byte for byte. Cosend does not define, validate or version this body; read it against the Graph API docs for the version in the path. application/json
GET /{version}/*
Graph passthrough: unlisted and blocked paths answer 404, nothing is forwarded
Scopes runtime:proxy
POST /{version}/*
Graph passthrough: unlisted and blocked paths answer 404, nothing is forwarded
Scopes runtime:proxy
PUT /{version}/*
Graph passthrough: unlisted and blocked paths answer 404, nothing is forwarded
Scopes runtime:proxy
PATCH /{version}/*
Graph passthrough: unlisted and blocked paths answer 404, nothing is forwarded
Scopes runtime:proxy
DELETE /{version}/*
Graph passthrough: unlisted and blocked paths answer 404, nothing is forwarded
Scopes runtime:proxy
GET /{version}/media/{media_id}/content
Stream a media object through the Cosend credential. A media lookup returns this address as its url in place of the one Meta gave, so the caller can fetch it
Scopes runtime:proxy
Response 200
The media object itself, under the content type Meta served it with. Fetched through the Cosend credential rather than the caller's. application/octet-stream
/authorize
GET /authorize
Start the MCP sign-in: checks the request and opens the consent screen
Scopes none; the contract declares no credential
Response 200
A page that sends the browser on to the Cosend consent screen. A request that cannot be accepted is sent back to the AI app with an OAuth error (302) when the app’s details were read from its own address; for an app that registered itself, or one that cannot be verified, it is shown as a 400 page. Too many sign-ins started from one address answer a 429 page. text/html; charset=utf-8
/free
GET /free
Refuses: the GET stream was removed in MCP 2026-07-28
Scopes none; the contract declares no credential
POST /free
The free-tools MCP endpoint: no sign-in, only the WhatsApp link, template check and message cost
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| jsonrpc | "2.0" | required | |
| id | string | number | null | required | |
| result | object | required | |
| result.resultType | "complete" | optional | On every 2026-07-28 result. A 2025-era request (after initialize) gets that era’s plain result, which has none. |
Response 202
No content
DELETE /free
Refuses: there are no sessions to delete in MCP 2026-07-28
Scopes none; the contract declares no credential
/mcp
GET /mcp
Refuses: the GET stream was removed in MCP 2026-07-28
Scopes none; the contract declares no credential
POST /mcp
The MCP endpoint (Streamable HTTP: 2026-07-28, and 2025-11-25 through initialize)
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| jsonrpc | "2.0" | required | |
| id | string | number | null | required | |
| result | object | required | |
| result.resultType | "complete" | optional | On every 2026-07-28 result. A 2025-era request (after initialize) gets that era’s plain result, which has none. |
Response 202
No content
DELETE /mcp
Refuses: there are no sessions to delete in MCP 2026-07-28
Scopes none; the contract declares no credential
/public
GET /public/v1/site-pages
The published marketing pages, for the site build: each one’s Markdown and images
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| data | object[] | required | |
| data[].id | string | required | |
| data[].section | "guides" | "compare" | "docs-guides" | "use-cases" | "blog" | "tools" | required | |
| data[].slug | string | required | |
| data[].markdown | string | required | |
| data[].published_at | string (date-time) | required | |
| data[].updated_at | string (date-time) | required | |
| data[].images | object[] | required | |
| data[].images[].name | string | required | |
| data[].images[].url | string | required |
GET /public/v1/site-pages/images/{imageId}
An image a published marketing page shows. Any other image is 404
Scopes none; the contract declares no credential
Response 200
The image as it was uploaded (PNG, JPEG, WebP or GIF), with its own Content-Type. image/*
/register
POST /register
Register an AI app for the MCP sign-in (RFC 7591); public clients, PKCE only
Scopes none; the contract declares no credential
Response 201
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| client_id | string | required | |
| client_id_issued_at | integer | required | |
| client_name | string | optional | |
| redirect_uris | string[] | required | |
| application_type | string | optional | |
| token_endpoint_auth_method | "none" | required | |
| grant_types | string[] | required | |
| response_types | string[] | required |
/token
POST /token
Exchange an MCP authorization code or refresh token for an access token
Scopes none; the contract declares no credential
Response 200
OK application/json
| Field | Type | Required | Description |
|---|---|---|---|
| access_token | string | required | |
| token_type | "Bearer" | required | |
| expires_in | integer | required | |
| refresh_token | string | required | |
| scope | string | required |