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.

an error on /v1
{
  "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 ten 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, so 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.

Some codes add a member, and only the ones that name it. Beyond the fixed members above, error carries whatever the specific code calls for: an upgrade_url on quota_exceeded, a paused_until on template_not_approved, a held_by on conversation_locked. The one to handle deliberately is upstream_error: on a 502 it is Meta's response body, unchanged, so a failure at Meta reaches you with Meta's own diagnosis attached instead of being flattened into ours.

A resource in another workspace 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 ten types

The ten error types, the HTTP status each maps to, and what it means.
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 or role. Never a plan or billing state; that is not_entitled
not_entitled 403 The credential is fine and the request is legal; the workspace is not currently entitled to this feature: a plan gate, or an overdue account reaching for a paid capability. Non-payment never gates a free capability, and never gates the passthrough. Always carries feature and upgrade_url
not_found 404 Does not exist, or exists in another workspace
conflict 409 Idempotency conflict, or a state transition that is not legal
quota 402 A plan limit or cap was reached. Body names the meter and the upgrade URL
rate_limit 429 Ours or Meta's. Retry-After set
upstream 502 Meta or a connector failed. upstream_error carries their payload verbatim
internal 500 Ours. request_id is the only useful thing; never leaks internals

Authentication and permission

missing_credential 401 authentication
No Authorization header.
invalid_credential 401 authentication
Unrecognised, malformed or revoked.
expired_credential 401 authentication
Past its expiry.
insufficient_scope 403 permission
The credential is valid but lacks the scope. The body names which one.
insufficient_role 403 permission
The member’s role does not permit this.
test_key_on_live_connection 403 permission
A test credential against a live connection. The credential is the mode, so there is no header to forget.
keys_write_not_available_to_oauth 403 permission
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.
invalid_verify_token 403 permission
A GET /webhooks/meta whose hub.verify_token does not match. Not invalid_signature: a bad token on the subscription check and a bad signature on a delivery are different failures.
invalid_signature 401 authentication
A webhook whose HMAC does not verify.

Entitlement

plan_does_not_include 403 not_entitled
The feature is not on this plan: the shared inbox on Free, for instance. The body carries an upgrade URL.
account_overdue 403 not_entitled
The account is past due or unpaid and the capability asked for is a paid one. Non-payment reduces an account to its free capabilities: it never refuses a free one, and never the passthrough. The body carries a billing URL.
not_entitled 403 not_entitled
The generic entitlement refusal, raised where a capability is gated and no more specific code applies. Carries feature, reason and upgrade_url.

Request

invalid_request 400 invalid_request
Schema validation failed. details[] carries up to ten JSON pointers and param is the first.
unknown_field 400 invalid_request
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_field 400 invalid_request
A field the schema requires was absent.
invalid_cursor 400 invalid_request
A malformed or non-existent after / before.
idempotency_key_required 400 invalid_request
Send endpoints require one. A retried send with no key is a duplicate message to a real person.
idempotency_key_reuse 409 conflict
The same key with a different body. Silently returning the first response would swallow a second, different intent.
idempotency_key_in_flight 409 conflict
The first request with this key is still running. Retry-After is 1.
rate_limited 429 rate_limit
Our own rate limit, per credential. Wait for the Retry-After interval (also given as retry_after_ms) and retry.
not_implemented 501 internal
A route that exists and is not built yet: DELETE /v1/contacts/{id}?purge=true returns it until the cascade behind it is built.
not_found 404 not_found
Absent, or another workspace’s.
sole_owner 409 conflict
Demoting or removing the last owner, which would leave the workspace with no owner. Transfer ownership first with POST /v1/memberships/transfer-owner.
unsupported_api_version 400 invalid_request
A passthrough version segment outside the supported range.

Connection and messaging

connection_not_found 404 not_found
No connection with that ID, or it belongs to another workspace.
connection_not_active 409 conflict
The connection needs reauthorisation, is disconnected, or is payment-blocked.
connection_not_routable 403 permission
The passthrough gate. reason names which of the seven applies, and the Runtime API reference lists them all.
not_supported_on_coexistence 403 permission
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_conflicts 409 conflict
Switching this connection to direct delivery would break the objects listed.
marketing_not_available_on_direct 403 permission
A marketing-category send on a direct connection, where marketing sends are not available. Nobody opted out, so there is no consent record to fix.
direct_mode_no_content 409 conflict
An endpoint that needs message content, on a connection that stores none. Direct-delivery mode writes metadata and never a body.
service_window_closed 409 conflict
A free-form message outside the 24-hour window with no template fallback. Maps Meta 131047.
template_not_found 404 not_found
No template with that name or ID.
template_not_approved 409 conflict
Not approved, or paused. Carries paused_until when Meta gave one.
recipient_opted_out 409 conflict
Consent was withdrawn for this category.
throughput_exceeded 429 rate_limit
The connection’s messages-per-second ceiling: 20 on coexistence, fixed and not raisable. Maps Meta 130429.
pair_rate_limited 429 rate_limit
The same recipient, too often. Maps Meta 131056.
identity_changed 409 conflict
The contact’s identity key changed. Maps Meta 137000.
conversation_locked 409 conflict
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_permitted 403 permission
A forced take-over from a member. Only an owner or an admin may seize an active lock.
lock_request_expired 409 conflict
Declining a take-over request after its sixty seconds. It has already transferred.
lock_request_not_yours 403
Granting or declining a take-over request from anyone who is not the lock’s holder. Only the holder answers a request.
handover_is_one_way 409
A transition back to automated control. There is no path from a human back to the AI.
control_app_is_observed 400 invalid_request
Setting control to app. It is observed from smb_message_echoes and never set.
media_too_large 413 invalid_request
Over the 100 MB ceiling, which is Meta’s own limit rather than ours.
media_unavailable 410 not_found
Meta’s asset expired and no copy of it exists in our storage.
connection_credential_unusable 424 conflict
The connection’s stored credential cannot be used.

Automation, quota and upstream

automation_invalid 400 invalid_request
Validation failed. The body is the full validation report, not a single message.
automation_not_deployed 409 conflict
The automation has no active version to trigger.
connector_not_connected 409 conflict
An action needs a connector account that does not exist.
connector_needs_reauth 409 conflict
The credential is no longer valid at the provider.
approval_required 202 conflict
Held for a human decision. The body carries the approval’s ID and its URL. Not a failure: the run is waiting.
approval_expired 409 conflict
The held approval was not answered in time.
quota_exceeded 402 quota
Included units are used and the cap is reached. The body names the meter, the usage, the cap and the upgrade URL.
ai_conversations_capped 402 quota
The Free plan's AI ceiling. AI replies stop; the inbox, the API and transactional sends keep working. Paid plans have no AI ceiling: going past what is included is billed rather than refused.
concurrency_limit 429 rate_limit
The workspace’s concurrent-run limit.
upstream_error 502 upstream
Meta or a connector failed. upstream_error carries their payload verbatim, so you can handle our vocabulary and still read theirs.
upstream_timeout 504 upstream
Meta or a connector did not answer in time. On the Runtime API that is thirty seconds, connect plus read.
internal_error 500 internal
Ours. request_id is the only content, and it is the one to quote to support.

Approvals, notifications and data requests

approval_action_type_unknown 400 invalid_request
An action_type that is not in the registry. GET /v1/approval-action-types lists the ones that are.
approval_already_decided 409 conflict
A second decision on an approval that a colleague, or the expiry sweep, has already answered. Refresh to see the outcome.
notification_not_cancellable 409 conflict
Cancelling a notification that is no longer scheduled or queued.
recurring_schedules_not_available 409 conflict
A recurring schedule_kind. Recurring schedules are not available yet.
queue_depth_exceeded 429 rate_limit
The notification queue is full and is shedding load. Retry later.
erasure_already_queued 409 conflict
A second erasure request while one is already queued.