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 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
| 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_credential401 authentication - No Authorization header.
-
invalid_credential401 authentication - Unrecognised, malformed or revoked.
-
expired_credential401 authentication - Past its expiry.
-
insufficient_scope403 permission - The credential is valid but lacks the scope. The body names which one.
-
insufficient_role403 permission - The member’s role does not permit this.
-
test_key_on_live_connection403 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_oauth403 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_token403 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_signature401 authentication - A webhook whose HMAC does not verify.
Entitlement
-
plan_does_not_include403 not_entitled - The feature is not on this plan: the shared inbox on Free, for instance. The body carries an upgrade URL.
-
account_overdue403 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_entitled403 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_request400 invalid_request - Schema validation failed. details[] carries up to ten JSON pointers and param is the first.
-
unknown_field400 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_field400 invalid_request - A field the schema requires was absent.
-
invalid_cursor400 invalid_request - A malformed or non-existent after / before.
-
idempotency_key_required400 invalid_request - Send endpoints require one. A retried send with no key is a duplicate message to a real person.
-
idempotency_key_reuse409 conflict - The same key with a different body. Silently returning the first response would swallow a second, different intent.
-
idempotency_key_in_flight409 conflict - The first request with this key is still running. Retry-After is 1.
-
rate_limited429 rate_limit - Our own rate limit, per credential. Wait for the Retry-After interval (also given as retry_after_ms) and retry.
-
not_implemented501 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_found404 not_found - Absent, or another workspace’s.
-
sole_owner409 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_version400 invalid_request - A passthrough version segment outside the supported range.
Connection and messaging
-
connection_not_found404 not_found - No connection with that ID, or it belongs to another workspace.
-
connection_not_active409 conflict - The connection needs reauthorisation, is disconnected, or is payment-blocked.
-
connection_not_routable403 permission - The passthrough gate. reason names which of the seven applies, and the Runtime API reference lists them all.
-
not_supported_on_coexistence403 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_conflicts409 conflict - Switching this connection to direct delivery would break the objects listed.
-
marketing_not_available_on_direct403 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_content409 conflict - An endpoint that needs message content, on a connection that stores none. Direct-delivery mode writes metadata and never a body.
-
service_window_closed409 conflict - A free-form message outside the 24-hour window with no template fallback. Maps Meta 131047.
-
template_not_found404 not_found - No template with that name or ID.
-
template_not_approved409 conflict - Not approved, or paused. Carries paused_until when Meta gave one.
-
recipient_opted_out409 conflict - Consent was withdrawn for this category.
-
throughput_exceeded429 rate_limit - The connection’s messages-per-second ceiling: 20 on coexistence, fixed and not raisable. Maps Meta 130429.
-
pair_rate_limited429 rate_limit - The same recipient, too often. Maps Meta 131056.
-
identity_changed409 conflict - The contact’s identity key changed. Maps Meta 137000.
-
conversation_locked409 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_permitted403 permission - A forced take-over from a member. Only an owner or an admin may seize an active lock.
-
lock_request_expired409 conflict - Declining a take-over request after its sixty seconds. It has already transferred.
-
lock_request_not_yours403 - 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_way409 - A transition back to automated control. There is no path from a human back to the AI.
-
control_app_is_observed400 invalid_request - Setting control to app. It is observed from smb_message_echoes and never set.
-
media_too_large413 invalid_request - Over the 100 MB ceiling, which is Meta’s own limit rather than ours.
-
connection_credential_unusable424 conflict - The connection’s stored credential cannot be used.
Automation, quota and upstream
-
automation_invalid400 invalid_request - Validation failed. The body is the full validation report, not a single message.
-
automation_not_deployed409 conflict - The automation has no active version to trigger.
-
connector_not_connected409 conflict - An action needs a connector account that does not exist.
-
connector_needs_reauth409 conflict - The credential is no longer valid at the provider.
-
approval_required202 conflict - Held for a human decision. The body carries the approval’s ID and its URL. Not a failure: the run is waiting.
-
approval_expired409 conflict - The held approval was not answered in time.
-
quota_exceeded402 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_capped402 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_limit429 rate_limit - The workspace’s concurrent-run limit.
-
upstream_error502 upstream - Meta or a connector failed. upstream_error carries their payload verbatim, so you can handle our vocabulary and still read theirs.
-
upstream_timeout504 upstream - Meta or a connector did not answer in time. On the Runtime API that is thirty seconds, connect plus read.
-
internal_error500 internal - Ours. request_id is the only content, and it is the one to quote to support.
Approvals, notifications and data requests
-
risk_consent_required400 invalid_request - PATCH /v1/approval-settings without risk_acknowledged on a mode that requires it. Show the risk, get the acknowledgement, and resend.
-
approval_action_type_unknown400 invalid_request - An action_type that is not in the registry. GET /v1/approval-action-types lists the ones that are.
-
approval_already_decided409 conflict - A second decision on an approval that a colleague, or the expiry sweep, has already answered. Refresh to see the outcome.
-
notification_not_cancellable409 conflict - Cancelling a notification that is no longer scheduled or queued.
-
recurring_schedules_not_available409 conflict - A recurring schedule_kind. Recurring schedules are not available yet.
-
queue_depth_exceeded429 rate_limit - The notification queue is full and is shedding load. Retry later.
-
erasure_already_queued409 conflict - A second erasure request while one is already queued.
-
cancel_link_used410 conflict - A one-time cancellation link that has already been used.
-
cancel_link_expired410 conflict - A cancellation link that is past its window.
-
export_link_used410 conflict - A one-time export link that has already been redeemed.