Error reference
Every code the API can return. The docs field in an error envelope links
straight to the entry for that code, so an error message is one click from what it means.
The envelope
Every error on /v1 has the same shape. On the Graph passthrough you get
Meta's envelope instead, unchanged, because an existing integration's
error handling has to keep working — the one exception is the routing gate, which is
documented separately and is written
in Meta's shape for the same reason.
{
"error": {
"type": "conflict",
"code": "connection_not_active",
"message": "This connection is not currently active.",
"param": "connection_id",
"request_id": "req_01K3F8QY7ZC4M2N9V6XJ0RTBHE",
"docs": "https://cosend.app/docs/errors#connection_not_active"
}
} Branch on type, log code. The nine types are a
closed set and they are what your retry and error-handling logic should switch on. The codes
are the specific reason, and new ones are added over time — treating an unfamiliar code as
fatal is how a client breaks on a release that broke nothing.
request_id is in the body, in the response header, and in our logs. Quoting it
to support starts the conversation at the right request rather than the right hour.
A resource in another organization returns 404, not
403. A 403 would confirm that the ID exists somewhere in
the system, which turns a guess into an oracle. This is deliberate, and it is the one
behaviour on this page that reads like a bug and is not.
The nine types
| Type | HTTP | Meaning |
|---|---|---|
| invalid_request | 400 | Malformed or semantically wrong input |
| authentication | 401 | Missing, bad, revoked or expired credential |
| permission | 403 | Authenticated, but not allowed — scope, role or plan |
| not_found | 404 | Does not exist, or exists in another organization |
| conflict | 409 | An idempotency conflict, or a state transition that is not legal |
| quota | 402 | A plan limit or cap was reached. The body names the meter and the upgrade URL |
| rate_limit | 429 | Ours or Meta’s. Retry-After is set |
| upstream | 502 | Meta or a connector failed. upstream_error carries their payload verbatim |
| internal | 500 | Ours. request_id is the only useful thing, and nothing internal leaks |
Authentication and permission
-
missing_credential401 - No Authorization header.
-
invalid_credential401 - Unrecognised, malformed or revoked.
-
expired_credential401 - Past its expiry.
-
insufficient_scope403 - The credential is valid but lacks the scope. The body names which one.
-
insufficient_role403 - The member’s role does not permit this.
-
test_key_on_live_connection403 - A test credential against a live connection. The credential is the mode, so there is no header to forget.
-
plan_does_not_include403 - The feature is not on this plan — the MCP server on Free, for instance. The body carries an upgrade URL.
-
keys_write_not_available_to_oauth403 - keys:write is never granted to an OAuth token. A client that could mint API keys could turn a scoped, revocable, audited consent into a permanent unaudited credential.
Request
-
invalid_request400 - Schema validation failed. details[] carries up to ten JSON pointers and param is the first.
-
unknown_field400 - A field the schema does not define. Bodies are strict, so a mistyped template_nmae fails at 10am rather than silently sending the wrong thing at 3am.
-
missing_required_field400 - A field the schema requires was absent.
-
invalid_cursor400 - A malformed or non-existent after / before.
-
idempotency_key_required400 - Send endpoints require one. A retried send with no key is a duplicate message to a real person.
-
idempotency_key_reuse409 - The same key with a different body. Silently returning the first response would swallow a second, different intent.
-
idempotency_key_in_flight409 - The first request with this key is still running. Retry-After is 1.
-
not_found404 - Absent, or another organization’s.
-
unsupported_api_version400 - A passthrough version segment outside the supported range.
Connection and messaging
-
connection_not_found404 - No connection with that ID — or it belongs to another organization.
-
connection_not_active409 - The connection needs reauthorisation, is disconnected, or is payment-blocked.
-
connection_not_routable403 - The passthrough gate. reason names which of the seven applies — the Runtime API reference lists them all.
-
not_supported_on_coexistence403 - Groups, calls, catalogs, register and deregister. These are what coexistence costs on the API side; the catalog and the profile keep working in the app.
-
direct_mode_conflicts409 - Switching this connection to direct delivery would break the objects listed.
-
direct_mode_no_content409 - An endpoint that needs message content, on a connection that stores none. Direct-delivery mode writes metadata and never a body.
-
service_window_closed409 - A free-form message outside the 24-hour window with no template fallback. Maps Meta 131047.
-
template_not_found404 - No template with that name or ID.
-
template_not_approved409 - Not approved, or paused. Carries paused_until when Meta gave one.
-
recipient_opted_out409 - Consent was withdrawn for this category.
-
throughput_exceeded429 - The connection’s messages-per-second ceiling — 20 on coexistence, fixed and not raisable. Maps Meta 130429.
-
pair_rate_limited429 - The same recipient, too often. Maps Meta 131056.
-
identity_changed409 - The contact’s identity key changed. Maps Meta 137000.
-
conversation_locked409 - Another agent holds the lease and is active. The body carries who holds it, when it expires, and the URL to request it.
-
lock_force_not_permitted403 - A forced take-over from a member. Only an owner or an admin may seize an active lock.
-
lock_request_expired409 - Declining a take-over request after its sixty seconds — it has already transferred.
-
media_too_large413 - Over the 100 MB ceiling, which is Meta’s own limit rather than ours.
Automation, quota and upstream
-
automation_invalid400 - Validation failed. The body is the full validation report, not a single message.
-
automation_not_deployed409 - The automation has no active version to trigger.
-
connector_not_connected409 - An action needs a connector account that does not exist.
-
connector_needs_reauth409 - The credential is no longer valid at the provider.
-
approval_required202 - Held for a human decision. The body carries the approval’s ID and its URL. Not a failure — the run is waiting.
-
approval_expired409 - The held approval was not answered in time.
-
quota_exceeded402 - Included units are used and the cap is reached. The body names the meter, the usage, the cap and the upgrade URL.
-
ai_conversations_capped402 - The AI overage cap. AI replies stop; the inbox, the API and transactional sends keep working.
-
concurrency_limit429 - The organization’s concurrent-run limit.
-
upstream_error502 - Meta or a connector failed. upstream_error carries their payload verbatim, so you can handle our vocabulary and still read theirs.
-
upstream_timeout504 - Meta or a connector did not answer in time. On the Runtime API that is thirty seconds, connect plus read.
-
internal_error500 - Ours. request_id is the only content, and it is the one to quote to support.