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

Response 200 fields of GET /v1/account/data-policy.
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

Request body of PATCH /v1/account/data-policy.
Field Type Required Description
conversation_data_months integer optional
operational_records_months integer optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/account/data-policy.
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

Request body of POST /v1/account/erasure.
Field Type Required Description
confirm string required

Response 202

OK application/json

Response 202 fields of POST /v1/account/erasure.
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

Response 200 fields of DELETE /v1/account/erasure.
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

Response 200 fields of POST /v1/account/erasure/cancel/{token}.
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

Parameters of GET /v1/ai/authoring.
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

Response 200 fields of GET /v1/ai/authoring.
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

Request body of POST /v1/ai/authoring.
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

Response 201 fields of POST /v1/ai/authoring.
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

Response 200 fields of GET /v1/ai/authoring/{id}.
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

Request body of POST /v1/ai/authoring/{id}/messages.
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

Response 200 fields of POST /v1/ai/authoring/{id}/messages.
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

Request body of POST /v1/ai/try.
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

Response 200 fields of POST /v1/ai/try.
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

Response 200 fields of GET /v1/ai-connections.
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

Response 200 fields of DELETE /v1/ai-connections/{id}.
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

Response 200 fields of GET /v1/api-keys.
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

Request body of POST /v1/api-keys.
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

Response 201 fields of POST /v1/api-keys.
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

Response 200 fields of DELETE /v1/api-keys/{id}.
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

Request body of POST /v1/api-keys/{id}/rotate.
Field Type Required Description
grace_hours integer optional

Response 200

OK application/json

Response 200 fields of POST /v1/api-keys/{id}/rotate.
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

Response 200 fields of GET /v1/approval-action-types.
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

Response 200 fields of GET /v1/approval-settings.
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

Request body of PATCH /v1/approval-settings.
Field Type Required Description
action_type string required
mode "ask" | "auto_approve" | "auto_reject" required
risk_acknowledged boolean optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/approval-settings.
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

Parameters of GET /v1/approvals.
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

Response 200 fields of GET /v1/approvals.
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

Parameters of POST /v1/approvals.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/approvals.
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

Response 201 fields of POST /v1/approvals.
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

Response 200 fields of GET /v1/approvals/{id}.
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

Request body of POST /v1/approvals/{id}/approve.
Field Type Required Description
note string optional

Response 200

OK application/json

Response 200 fields of POST /v1/approvals/{id}/approve.
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

Request body of POST /v1/approvals/{id}/reject.
Field Type Required Description
note string optional

Response 200

OK application/json

Response 200 fields of POST /v1/approvals/{id}/reject.
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

Response 200 fields of GET /v1/auth/invitation.
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

Request body of POST /v1/auth/join-request.
Field Type Required Description
user_name string required

Response 202

OK application/json

Response 202 fields of POST /v1/auth/join-request.
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

Request body of POST /v1/auth/provision.
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

Response 200 fields of POST /v1/auth/provision.
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

Request body of POST /v1/auth/refresh.
Field Type Required Description
organization_id string optional

Response 200

OK application/json

Response 200 fields of POST /v1/auth/refresh.
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

Request body of POST /v1/auth/repeated-signup.
Field Type Required Description
email 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

Request body of POST /v1/auth/workspace-address.
Field Type Required Description
slug string required

Response 200

OK application/json

Response 200 fields of POST /v1/auth/workspace-address.
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

Response 200 fields of GET /v1/automation-catalogue.
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

Parameters of GET /v1/automations.
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

Response 200 fields of GET /v1/automations.
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

Parameters of POST /v1/automations.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/automations.
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

Response 201 fields of POST /v1/automations.
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

Parameters of GET /v1/automations/{id}.
Parameter In Type Required Description
expand query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/automations/{id}.
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

Request body of PATCH /v1/automations/{id}.
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

Response 200 fields of PATCH /v1/automations/{id}.
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

Response 200 fields of DELETE /v1/automations/{id}.
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

Parameters of POST /v1/automations/{id}/copy.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/automations/{id}/copy.
Field Type Required Description
name string optional The copy’s name. Absent: the original’s, followed by “(copy)”.

Response 201

OK application/json

Response 201 fields of POST /v1/automations/{id}/copy.
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

Request body of POST /v1/automations/{id}/deploy.
Field Type Required Description
version_id string optional
confirm_actions string[] optional

Response 200

OK application/json

Response 200 fields of POST /v1/automations/{id}/deploy.
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

Response 200 fields of POST /v1/automations/{id}/disable.
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

Request body of POST /v1/automations/{id}/enable.
Field Type Required Description
version_id string optional

Response 200

OK application/json

Response 200 fields of POST /v1/automations/{id}/enable.
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

Parameters of GET /v1/automations/{id}/knowledge.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/automations/{id}/knowledge.
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

Parameters of POST /v1/automations/{id}/knowledge.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/automations/{id}/knowledge.
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

Response 201 fields of POST /v1/automations/{id}/knowledge.
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

Response 200 fields of POST /v1/automations/{id}/knowledge/{sourceId}/refresh.
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

Request body of PATCH /v1/automations/{id}/knowledge/suggestions/{suggestionId}.
Field Type Required Description
accepted boolean required
question string optional
answer string optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/automations/{id}/knowledge/suggestions/{suggestionId}.
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

Response 200 fields of GET /v1/automations/{id}/knowledge/summary.
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

Response 200 fields of GET /v1/automations/{id}/persona.
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

Request body of PATCH /v1/automations/{id}/persona.
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

Response 200 fields of PATCH /v1/automations/{id}/persona.
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

Request body of POST /v1/automations/{id}/rollback.
Field Type Required Description
to_version integer required

Response 200

OK application/json

Response 200 fields of POST /v1/automations/{id}/rollback.
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

Parameters of GET /v1/automations/{id}/stats.
Parameter In Type Required Description
range query "today" | "7d" optional

Response 200

OK application/json

Response 200 fields of GET /v1/automations/{id}/stats.
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

Parameters of POST /v1/automations/{id}/test.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/automations/{id}/test.
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

Response 201 fields of POST /v1/automations/{id}/test.
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

Request body of POST /v1/automations/{id}/validate.
Field Type Required Description
ir object required

Response 200

OK application/json

Response 200 fields of POST /v1/automations/{id}/validate.
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

Parameters of GET /v1/automations/{id}/versions.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/automations/{id}/versions.
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

Parameters of POST /v1/automations/{id}/versions.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/automations/{id}/versions.
Field Type Required Description
ir object required
change_note string | null optional

Response 201

OK application/json

Response 201 fields of POST /v1/automations/{id}/versions.
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

Request body of POST /v1/billing/card-setup.
Field Type Required Description
return_url string (uri) required

Response 200

OK application/json

Response 200 fields of POST /v1/billing/card-setup.
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

Request body of POST /v1/billing/checkout.
Field Type Required Description
plan_code string required
interval "month" | "year" optional
return_url string (uri) required

Response 200

OK application/json

Response 200 fields of POST /v1/billing/checkout.
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

Request body of POST /v1/billing/checkout/embedded.
Field Type Required Description
plan_code string required
interval "month" | "year" optional
return_url string (uri) required

Response 200

OK application/json

Response 200 fields of POST /v1/billing/checkout/embedded.
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

Request body of POST /v1/billing/downgrade-consent.
Field Type Required Description
to_plan_code string required
typed_approval string required
typed_name string required

Response 201

OK application/json

Response 201 fields of POST /v1/billing/downgrade-consent.
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

Response 200 fields of GET /v1/billing/invoices.
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

Response 200 fields of POST /v1/billing/invoices/{id}/pay.
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

Response 200 fields of GET /v1/billing/invoices/{invoiceId}.
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

Request body of POST /v1/billing/plan-change.
Field Type Required Description
plan_code string required
interval "month" | "year" optional
proration_date string (date-time) optional

Response 200

OK application/json

Response 200 fields of POST /v1/billing/plan-change.
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

Request body of POST /v1/billing/plan-change/preview.
Field Type Required Description
plan_code string required
interval "month" | "year" optional
proration_date string (date-time) optional

Response 200

OK application/json

Response 200 fields of POST /v1/billing/plan-change/preview.
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

Response 200 fields of GET /v1/billing/plans.
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

Request body of POST /v1/billing/portal.
Field Type Required Description
return_url string (uri) required

Response 200

OK application/json

Response 200 fields of POST /v1/billing/portal.
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

Response 200 fields of GET /v1/billing/subscription.
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

Parameters of GET /v1/connections.
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

Response 200 fields of GET /v1/connections.
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

Response 200 fields of GET /v1/connections/{id}.
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

Request body of PATCH /v1/connections/{id}.
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

Response 200 fields of PATCH /v1/connections/{id}.
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

Response 200 fields of DELETE /v1/connections/{id}.
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

Request body of POST /v1/connections/{id}/display-name.
Field Type Required Description
display_name string required

Response 200

OK application/json

Response 200 fields of POST /v1/connections/{id}/display-name.
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

Response 200 fields of GET /v1/connections/{id}/onboarding.
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

Response 200 fields of POST /v1/connections/{id}/payment-check.
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

Response 200 fields of GET /v1/connections/{id}/profile.
Field Type Required Description
about string | null required
description string | null required
address string | null required
email 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

Request body of PATCH /v1/connections/{id}/profile.
Field Type Required Description
about string optional
description string | null optional
address string | null optional
email 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

Response 200 fields of PATCH /v1/connections/{id}/profile.
Field Type Required Description
about string | null required
description string | null required
address string | null required
email 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

Response 200 fields of POST /v1/connections/{id}/reauth.
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

Response 200 fields of GET /v1/connections/{id}/reply-order.
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

Response 200 fields of POST /v1/connections/{id}/sync.
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

Response 200 fields of GET /v1/connections/{id}/timeline.
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

Request body of POST /v1/connections/{id}/two-step-pin.
Field Type Required Description
pin string required

Response 200

OK application/json

Response 200 fields of POST /v1/connections/{id}/two-step-pin.
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

Response 200 fields of GET /v1/connections/attempts/{id}.
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

Request body of POST /v1/connections/attempts/{id}/resume.
Field Type Required Description
phone_number_id string | null optional
two_step_pin string optional

Response 200

OK application/json

Response 200 fields of POST /v1/connections/attempts/{id}/resume.
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

Response 200 fields of GET /v1/connections/attempts/{id}/timeline.
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

Request body of POST /v1/connections/connect.
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

Response 201 fields of POST /v1/connections/connect.
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

Response 202 fields of POST /v1/connections/connect.
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

Parameters of GET /v1/connector-accounts.
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

Response 200 fields of GET /v1/connector-accounts.
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

Response 200 fields of GET /v1/connector-accounts/{id}.
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

Request body of PATCH /v1/connector-accounts/{id}.
Field Type Required Description
name string optional
config object optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/connector-accounts/{id}.
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

Response 200 fields of DELETE /v1/connector-accounts/{id}.
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

Parameters of GET /v1/connector-accounts/{id}/appointments-preview.
Parameter In Type Required Description
days query integer optional

Response 200

OK application/json

Response 200 fields of GET /v1/connector-accounts/{id}/appointments-preview.
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

Request body of PATCH /v1/connector-accounts/{id}/credential.
Field Type Required Description
api_key string optional
username string optional
password string optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/connector-accounts/{id}/credential.
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

Response 200 fields of DELETE /v1/connector-accounts/{id}/credential.
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

Response 200 fields of GET /v1/connector-accounts/{id}/forms.
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

Request body of POST /v1/connector-accounts/{id}/numbers.
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

Response 200 fields of POST /v1/connector-accounts/{id}/numbers.
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

Response 200 fields of DELETE /v1/connector-accounts/{id}/numbers/{connection_id}.
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

Response 200 fields of POST /v1/connector-accounts/{id}/picker.
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

Parameters of GET /v1/connector-accounts/{id}/sheets.
Parameter In Type Required Description
spreadsheet_id query string optional
limit query integer optional
after query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/connector-accounts/{id}/sheets.
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

Parameters of POST /v1/connector-accounts/{id}/sheets.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/connector-accounts/{id}/sheets.
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

Response 201 fields of POST /v1/connector-accounts/{id}/sheets.
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

Response 200 fields of POST /v1/connector-accounts/{id}/test.
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

Parameters of POST /v1/connector-accounts/connect.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/connector-accounts/connect.
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

Response 200 fields of POST /v1/connector-accounts/connect.
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

Response 200 fields of POST /v1/connector-accounts/grants/{id}/claim.
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

Parameters of GET /v1/connector-identities.
Parameter In Type Required Description
connection_id query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/connector-identities.
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

Response 200 fields of GET /v1/connectors.
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

Parameters of GET /v1/contact-fields.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/contact-fields.
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

Request body of POST /v1/contact-fields.
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

Response 201 fields of POST /v1/contact-fields.
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

Request body of PATCH /v1/contact-fields/{id}.
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

Response 200 fields of PATCH /v1/contact-fields/{id}.
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

Response 200 fields of DELETE /v1/contact-fields/{id}.
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

Parameters of GET /v1/contact-imports.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/contact-imports.
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

Request body of POST /v1/contact-imports.
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

Response 201 fields of POST /v1/contact-imports.
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

Response 200 fields of GET /v1/contact-imports/{id}.
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

Request body of PATCH /v1/contact-imports/{id}.
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

Response 200 fields of PATCH /v1/contact-imports/{id}.
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

Response 200 fields of POST /v1/contact-imports/{id}/preview.
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

Response 202 fields of POST /v1/contact-imports/{id}/run.
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

Parameters of GET /v1/contacts.
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

Response 200 fields of GET /v1/contacts.
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

Request body of POST /v1/contacts.
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

Response 200 fields of POST /v1/contacts.
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

Response 201 fields of POST /v1/contacts.
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

Response 200 fields of GET /v1/contacts/{id}.
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

Request body of PATCH /v1/contacts/{id}.
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

Response 200 fields of PATCH /v1/contacts/{id}.
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

Parameters of DELETE /v1/contacts/{id}.
Parameter In Type Required Description
purge query string optional

Response 200

OK application/json

Response 200 fields of DELETE /v1/contacts/{id}.
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

Parameters of GET /v1/contacts/{id}/consent.
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

Response 200 fields of GET /v1/contacts/{id}/consent.
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

Request body of POST /v1/contacts/{id}/consent.
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

Response 200 fields of POST /v1/contacts/{id}/consent.
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

Request body of POST /v1/contacts/{id}/merge.
Field Type Required Description
from string required

Response 200

OK application/json

Response 200 fields of POST /v1/contacts/{id}/merge.
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

Request body of POST /v1/contacts/{id}/tags.
Field Type Required Description
tags string[] required

Response 200

OK application/json

Response 200 fields of POST /v1/contacts/{id}/tags.
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

Request body of DELETE /v1/contacts/{id}/tags.
Field Type Required Description
tags string[] required

Response 200

OK application/json

Response 200 fields of DELETE /v1/contacts/{id}/tags.
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

Response 200 fields of GET /v1/contacts/{id}/threads.
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

Response 200 fields of GET /v1/contacts/{id}/window.
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

Parameters of GET /v1/conversations.
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

Response 200 fields of GET /v1/conversations.
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

Parameters of GET /v1/conversations/{id}.
Parameter In Type Required Description
expand query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/conversations/{id}.
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

Request body of PATCH /v1/conversations/{id}.
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

Response 200 fields of PATCH /v1/conversations/{id}.
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

Request body of POST /v1/conversations/{id}/control.
Field Type Required Description
control "pending_human" | "human" required
reason string optional
pause_bot_until string (date-time) optional

Response 200

OK application/json

Response 200 fields of POST /v1/conversations/{id}/control.
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

Request body of POST /v1/conversations/{id}/draft.
Field Type Required Description
instruction string | null optional

Response 200

OK application/json

Response 200 fields of POST /v1/conversations/{id}/draft.
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

Parameters of GET /v1/conversations/{id}/events.
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

Response 200 fields of GET /v1/conversations/{id}/events.
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

Request body of POST /v1/conversations/{id}/lock.
Field Type Required Description
activity boolean optional

Response 200

OK application/json

Response 200 fields of POST /v1/conversations/{id}/lock.
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

Response 200 fields of DELETE /v1/conversations/{id}/lock.
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

Request body of POST /v1/conversations/{id}/lock/requests.
Field Type Required Description
reason string optional

Response 201

OK application/json

Response 201 fields of POST /v1/conversations/{id}/lock/requests.
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

Parameters of GET /v1/conversations/{id}/notes.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/conversations/{id}/notes.
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

Request body of POST /v1/conversations/{id}/notes.
Field Type Required Description
body string required
mentions string[] optional

Response 201

OK application/json

Response 201 fields of POST /v1/conversations/{id}/notes.
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

Request body of PATCH /v1/conversations/{id}/notes/{noteId}.
Field Type Required Description
body string required
mentions string[] optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/conversations/{id}/notes/{noteId}.
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

Response 200 fields of POST /v1/conversations/{id}/read.
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

Response 200 fields of POST /v1/conversations/{id}/read-receipt.
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

Response 200 fields of POST /v1/conversations/{id}/resolve.
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

Response 200 fields of POST /v1/conversations/{id}/typing.
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

Request body of POST /v1/conversations/bulk.
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

Response 207 fields of POST /v1/conversations/bulk.
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

Parameters of GET /v1/conversations/counts.
Parameter In Type Required Description
connection_id query string | string[] optional

Response 200

OK application/json

Response 200 fields of GET /v1/conversations/counts.
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

Parameters of GET /v1/conversations/labels.
Parameter In Type Required Description
connection_id query string | string[] optional

Response 200

OK application/json

Response 200 fields of GET /v1/conversations/labels.
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

Parameters of GET /v1/events.
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

Response 200 fields of GET /v1/events.
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

Request body of POST /v1/exports.
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

Response 202 fields of POST /v1/exports.
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

Response 200 fields of GET /v1/exports/{id}.
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

Request body of POST /v1/inbox/session.
Field Type Required Description
take_over boolean optional
label string | null optional

Response 201

OK application/json

Response 201 fields of POST /v1/inbox/session.
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

Response 200 fields of POST /v1/inbox/session/{id}/heartbeat.
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

Response 200 fields of GET /v1/inbox-views.
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

Request body of POST /v1/inbox-views.
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

Response 201 fields of POST /v1/inbox-views.
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

Request body of PATCH /v1/inbox-views/{id}.
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

Response 200 fields of PATCH /v1/inbox-views/{id}.
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

Response 200 fields of POST /v1/lock-requests/{id}/decline.
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

Response 200 fields of POST /v1/lock-requests/{id}/grant.
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

Request body of POST /v1/mcp/consent.
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

Response 200 fields of POST /v1/mcp/consent.
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

Request body of POST /v1/mcp/consent/preview.
Field Type Required Description
request string required The request the dashboard was opened with: /authorize?request=….

Response 200

OK application/json

Response 200 fields of POST /v1/mcp/consent/preview.
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

Response 200 fields of GET /v1/me.
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

Request body of PATCH /v1/me.
Field Type Required Description
name string required

Response 200

OK application/json

Response 200 fields of PATCH /v1/me.
Field Type Required Description
id string required
email 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

Response 200 fields of GET /v1/me/preferences.
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

Request body of PATCH /v1/me/preferences.
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

Response 200 fields of PATCH /v1/me/preferences.
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

/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

Parameters of POST /v1/media.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/media.
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

Response 201 fields of POST /v1/media.
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

Response 200 fields of GET /v1/media/{id}.
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

Response 200 fields of GET /v1/memberships.
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

Request body of PATCH /v1/memberships/{id}.
Field Type Required Description
role "owner" | "admin" | "member" required

Response 200

OK application/json

Response 200 fields of PATCH /v1/memberships/{id}.
Field Type Required Description
id string required
object "membership" required
user_id string required
email 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

Response 200 fields of DELETE /v1/memberships/{id}.
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

Request body of POST /v1/memberships/invites.
Field Type Required Description
email string (email) required
role "owner" | "admin" | "member" required

Response 201

OK application/json

Response 201 fields of POST /v1/memberships/invites.
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

Response 200 fields of DELETE /v1/memberships/invites/{id}.
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

Response 200 fields of GET /v1/memberships/join-requests.
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

Response 201 fields of POST /v1/memberships/join-requests/{id}/approve.
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

Response 200 fields of POST /v1/memberships/join-requests/{id}/decline.
Field Type Required Description
id string required
object "join_request" required
email 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

Response 200 fields of GET /v1/memberships/me/access-requests.
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

Response 200 fields of POST /v1/memberships/me/access-requests.
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

Request body of POST /v1/memberships/transfer-owner.
Field Type Required Description
membership_id string required

Response 200

OK application/json

Response 200 fields of POST /v1/memberships/transfer-owner.
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

Parameters of GET /v1/messages.
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

Response 200 fields of GET /v1/messages.
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

Parameters of POST /v1/messages.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/messages.
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

Response 201 fields of POST /v1/messages.
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

Response 200 fields of GET /v1/messages/{id}.
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

Parameters of POST /v1/messages/{id}/reactions.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/messages/{id}/reactions.
Field Type Required Description
emoji string required

Response 201

OK application/json

Response 201 fields of POST /v1/messages/{id}/reactions.
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

Parameters of POST /v1/messages/{id}/read.
Parameter In Type Required Description
Idempotency-Key header string optional

Response 200

OK application/json

Response 200 fields of POST /v1/messages/{id}/read.
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

Request body of POST /v1/messages/{id}/translate.
Field Type Required Description
target_language string required

Response 200

OK application/json

Response 200 fields of POST /v1/messages/{id}/translate.
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

Parameters of POST /v1/messages/batch.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/messages/batch.
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

Response 207 fields of POST /v1/messages/batch.
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

Errors contact_belongs_to_other_connection

/v1/notifications

GET /v1/notifications

List notifications

Scopes notifications:read

Parameters

Parameters of GET /v1/notifications.
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

Response 200 fields of GET /v1/notifications.
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

Parameters of POST /v1/notifications.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/notifications.
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

Response 200 fields of POST /v1/notifications.
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

Response 201 fields of POST /v1/notifications.
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

Response 200 fields of GET /v1/notifications/{id}.
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

Response 200 fields of POST /v1/notifications/{id}/cancel.
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

Parameters of POST /v1/notifications/{id}/replay.
Parameter In Type Required Description
Idempotency-Key header string required

Response 201

OK application/json

Response 201 fields of POST /v1/notifications/{id}/replay.
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

Parameters of POST /v1/notifications/batch.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/notifications/batch.
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

Response 207 fields of POST /v1/notifications/batch.
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

Response 200 fields of GET /v1/organizations.
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

Request body of POST /v1/organizations.
Field Type Required Description
name string required

Response 201

OK application/json

Response 201 fields of POST /v1/organizations.
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

Request body of PATCH /v1/organizations/{id}.
Field Type Required Description
name string optional
timezone string optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/organizations/{id}.
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

Parameters of GET /v1/recipes.
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

Response 200 fields of GET /v1/recipes.
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

Request body of POST /v1/recipes/{slug}/instantiate.
Field Type Required Description
params object optional

Response 200

OK application/json

Response 200 fields of POST /v1/recipes/{slug}/instantiate.
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

Parameters of GET /v1/runs.
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

Response 200 fields of GET /v1/runs.
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

Parameters of POST /v1/runs.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/runs.
Field Type Required Description
automation_id string required
payload object optional

Response 201

OK application/json

Response 201 fields of POST /v1/runs.
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

Response 200 fields of GET /v1/runs/{id}.
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

Response 200 fields of POST /v1/runs/{id}/cancel.
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

Parameters of GET /v1/runs/{id}/events.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/runs/{id}/events.
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

Parameters of GET /v1/schedules.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/schedules.
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

Parameters of POST /v1/schedules.
Parameter In Type Required Description
Idempotency-Key header string required

Request body

Request body of POST /v1/schedules.
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

Response 201 fields of POST /v1/schedules.
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

Errors recurring_schedules_not_available

GET /v1/schedules/{id}

One schedule and its occurrences

Scopes notifications:read

Response 200

OK application/json

Response 200 fields of GET /v1/schedules/{id}.
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

Request body of PATCH /v1/schedules/{id}.
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

Response 200 fields of PATCH /v1/schedules/{id}.
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

Response 200 fields of DELETE /v1/schedules/{id}.
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

Parameters of GET /v1/schedules/{id}/occurrences.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/schedules/{id}/occurrences.
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

Response 200 fields of GET /v1/setup.
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

Request body of PATCH /v1/setup.
Field Type Required Description
hidden boolean optional
skipped ("connect" | "first_message" | "invite" | "first_automation")[] optional

Response 200

OK application/json

Response 200 fields of PATCH /v1/setup.
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

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

Response 200 fields of GET /v1/setup-links/{id}.
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

Response 200 fields of DELETE /v1/setup-links/{id}.
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

Parameters of GET /v1/stats/today.
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

Response 200 fields of GET /v1/stats/today.
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

Parameters of GET /v1/stats/week.
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

Response 200 fields of GET /v1/stats/week.
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

Parameters of GET /v1/stream.
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

Parameters of POST /v1/support-reports.
Parameter In Type Required Description
Idempotency-Key header string optional

Response 201

OK application/json

Response 201 fields of POST /v1/support-reports.
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

Parameters of GET /v1/templates.
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

Response 200 fields of GET /v1/templates.
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

Parameters of POST /v1/templates.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/templates.
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

Response 201 fields of POST /v1/templates.
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

Response 200 fields of GET /v1/templates/{id}.
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

Parameters of PATCH /v1/templates/{id}.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of PATCH /v1/templates/{id}.
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

Response 200 fields of PATCH /v1/templates/{id}.
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

Response 200 fields of DELETE /v1/templates/{id}.
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

Parameters of POST /v1/templates/{id}/copy.
Parameter In Type Required Description
Idempotency-Key header string optional

Request body

Request body of POST /v1/templates/{id}/copy.
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

Response 201 fields of POST /v1/templates/{id}/copy.
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

Response 200 fields of GET /v1/templates/category-changes.
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

Parameters of POST /v1/templates/category-changes/{id}/acknowledge.
Parameter In Type Required Description
Idempotency-Key header string optional

Response 200

OK application/json

Response 200 fields of POST /v1/templates/category-changes/{id}/acknowledge.
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

Request body of POST /v1/templates/sync.
Field Type Required Description
connection_id string required

Response 200

OK application/json

Response 200 fields of POST /v1/templates/sync.
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

Parameters of GET /v1/tools/message-cost.
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

Response 200 fields of GET /v1/tools/message-cost.
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

Request body of POST /v1/tools/template-check.
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

Response 200 fields of POST /v1/tools/template-check.
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

/v1/usage

GET /v1/usage

Usage for one billing period: used is live, allowances are plan configuration

Scopes usage:read

Parameters

Parameters of GET /v1/usage.
Parameter In Type Required Description
period query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/usage.
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

Response 200 fields of POST /v1/webhook-deliveries/{id}/replay.
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

Parameters of GET /v1/webhook-endpoints.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/webhook-endpoints.
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

Request body of POST /v1/webhook-endpoints.
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

Response 201 fields of POST /v1/webhook-endpoints.
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

Request body of PATCH /v1/webhook-endpoints/{id}.
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

Response 200 fields of PATCH /v1/webhook-endpoints/{id}.
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

Response 200 fields of DELETE /v1/webhook-endpoints/{id}.
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

Parameters of GET /v1/webhook-endpoints/{id}/deliveries.
Parameter In Type Required Description
limit query integer optional
after query string optional
before query string optional

Response 200

OK application/json

Response 200 fields of GET /v1/webhook-endpoints/{id}/deliveries.
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

Response 200 fields of POST /v1/webhook-endpoints/{id}/rotate-secret.
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

Response 200 fields of POST /v1/webhook-endpoints/{id}/test.
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

Response 200 fields of GET /.well-known/jwks.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

Response 200 fields of GET /.well-known/oauth-authorization-server.
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

Response 200 fields of GET /.well-known/oauth-protected-resource/mcp.
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

Response 200 fields of POST /free.
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

Response 200 fields of POST /mcp.
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

Response 200 fields of GET /public/v1/site-pages.
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

Response 201 fields of POST /register.
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

Response 200 fields of POST /token.
Field Type Required Description
access_token string required
token_type "Bearer" required
expires_in integer required
refresh_token string required
scope string required