REST API conventions

Credentials, scopes, response headers, cursor pagination, idempotency, strict validation, versioning and rate limits — the things that are true of every /v1 endpoint.

https://api.cosend.app/v1/…. JSON in, JSON out, snake_case throughout — the same casing Meta uses, so the passthrough and /v1 read alike.

This page is what holds for every endpoint. The endpoint-by-endpoint reference is generated from the same contracts package the API validates against, so it cannot drift from the API; it lands with the API rather than being hand-written here, because a hand-written copy would be a second source of truth and it would be wrong within a week.

Credentials

One header, Authorization: Bearer <credential>, and four kinds of credential.

The four credential kinds, what each grants and which surface accepts it.
CredentialGrantsWhere it works
Connection key csk_…Exactly one connectionRuntime API and /v1
Organization key cok_…Every connection in one organization/v1 only
OAuth access tokenOne organization, consent-scoped/v1, through the MCP server
Dashboard tokenOne organization, one member, that member’s role. Fifteen minutes/v1, from the browser

Every key is {prefix}_{live|test}_{32 characters}. The full string is shown once at creation and stored hashed — losing one means rotating it, not recovering it.

Test mode is the credential. A csk_test_ or cok_test_ key resolves to the same organization but is accepted only against connections marked as test, and its traffic is excluded from usage. A test key against a live connection returns test_key_on_live_connection. There is no test-mode header, so there is none to forget in production.

Scopes

Organization keys and OAuth tokens carry scopes; a connection key carries the connection-scoped set by default and can be narrowed at creation. They are the obvious pairs — messages:read / messages:write, and the same shape for contacts, templates, notifications, automations, runs, connections, connectors, webhooks — plus usage:read and runtime:proxy.

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. The dashboard is the only place a key is created.

On every response

The headers on every API response.
HeaderAlways
X-Request-IdThe same value appears in the body, in our log line and in our error tracking. Support starts here
X-RateLimit-Limit / -Remaining / -ResetThe bucket that governed this request
Retry-AfterOn 429 and on 503
Idempotency-ReplayedSet when a stored response was returned rather than a new one produced

Pagination

Cursor-based on the primary key. No offsets, ever — offsets skip and duplicate rows under concurrent writes, and an inbox is written to constantly.

GET /v1/messages?conversation_id=cnv_…&limit=50&after=msg_01K3F8QY7ZC4M2N9V6XJ0RTBHE
{
  "data": [ … ],
  "has_more": true,
  "next_cursor": "msg_01K3F8QY7ZC4M2N9V6XJ0RTBHF"
}

limit defaults to 25 and caps at 100. after walks forward, before walks backward, next_cursor is null when has_more is false. Treat the cursor as opaque even though it is a readable ID — that is what lets it change shape later without breaking you.

There is no total count and there will not be one, and therefore no “page 4 of 17”. Counting a filtered set on every list request is a scan of the largest tables in the system, and the number is wrong by the time it renders. Build a Load more against has_more. The one place a total is legitimate is the usage endpoint, where the count is the resource.

Idempotency

Every side-effecting POST accepts Idempotency-Key, and on send endpoints it is required — a retried send with no key is a duplicate message to a real person.

  • Keys are stored for 24 hours.
  • The stored record includes a hash of the request. The same key with a different body returns idempotency_key_reuse, naming the key. Silently returning the first response would swallow a second, different intent.
  • A replay while the first request is still running returns idempotency_key_in_flight with Retry-After: 1.
  • A completed replay returns the original status and body, with Idempotency-Replayed set.

Validation

Every body is parsed against a schema. A failure is 400 with param set to the pointer of the first offending field and up to ten entries in details[].

Unknown fields are rejected, not ignored. A mistyped template_nmae fails loudly at 10am rather than silently sending the wrong thing at 3am. The one exception is metadata objects, which are open by design — they are yours, they are stored, they are echoed back on webhooks, and they are never sent to Meta.

Versioning

/v1 is additive only. Adding a field, an optional parameter or an enum value is not a breaking change, and clients must tolerate fields they do not recognise. A breaking change means /v2, announced twelve months ahead, with both live throughout.

On the passthrough the version is Meta’s and you pick it.

Rate limits

The rate-limit buckets and their defaults.
BucketDefault
Per connection key100 requests/second, burst 200
Per organization key200 requests/second, burst 400
Per OAuth token20 requests/second, burst 40
Per connection, outbound to MetaThe connection’s throughput ceiling — 20 a second on coexistence
Per IP, unauthenticated20 requests/minute

A 429 from our limiter uses the /v1 envelope. A 429 that originated at Meta is passed through with Meta’s payload and a Retry-After derived from their headers where they sent any.

Errors

Nine type values, forty-seven code values, and every envelope carries a link to the entry for its own code. That is the error reference, and it is worth reading once before you write the retry logic rather than after.

Resources

The /v1 resources and the operations on each.
ResourceEndpoints
Identitythe current organization, member and scopes
Connectionslist, get, update, delete, connect, sync, reauthorise
Messagessend, list, get, mark read, react, typing
Conversationslist, get, update, assign, control, notes, tags
Contactscreate, read, update, delete, merge, consent, window
Templateslist, get, create, delete, sync
Mediaupload, get, download
Notificationssend, batch, get, list, cancel, replay
Schedulescreate, read, update, delete, occurrences
Automationscreate, read, update, delete, versions, deploy, roll back, enable
Runslist, get, trigger, cancel, events
Connectorscatalogue, accounts, connect, disconnect
Approvalslist, approve, reject
Usageget, breakdown
Webhook endpointscreate, read, update, delete, test, deliveries, replay
API keyslist, create, revoke, rotate
Eventsthe audit log
Memberships and inviteslist, invite, change role, remove, transfer ownership
Billingsubscription, checkout, portal

Every list endpoint takes limit, after, before, created_after, created_before and order. List and get endpoints take expand= with a comma-separated list of relation names — GET /v1/conversations?expand=contact,last_message — so one round trip replaces N+1.