Runtime API reference

The Graph-compatible passthrough — version handling, path validation, the routing gate and its seven reasons, headers, limits, and what gets recorded on the way past.

api.cosend.app proxies an allowlisted set of Meta Graph paths byte-compatibly. Same paths, same payloads, same error envelopes. This page is the reference; the Runtime API page is the argument for it, and migrating is the ninety-second version.

Shape

https://api.cosend.app/v{n}.{m}/{path}
Authorization: Bearer csk_live_…

Every allowlisted request is forwarded to https://graph.facebook.com/v{n}.{m}/{path} with:

  • the connection’s real Meta access token substituted into Authorization;
  • appsecret_proof computed server-side and appended — you never hold our app secret, and this is a piece of security work you would otherwise have to do yourself;
  • the request body forwarded byte for byte;
  • Meta’s response returned byte for byte: status code, error envelope, and the headers listed below.

A connection key, and only a connection key. An organization key is refused here. A Graph request names its target with the phone-number ID in the path, so if an org-wide credential were accepted that path segment would be selecting the connection — and a bug in that resolution is a cross-tenant leak. With a connection key the connection is already known and the path is only ever checked against it.

Version handling

You choose. The segment is validated against ^v\d{1,2}\.\d{1,2}$ and against a supported range, then forwarded verbatim. That is what makes this a drop-in for an integration pinned to v25.0.

  • Supported range: the newest three Graph versions Meta lists as available, plus any version Meta still serves.
  • Out of range400 in Meta’s envelope with unsupported_api_version.
  • Cosend’s own internal calls use a pinned version of their own, independently of whatever you send.

What is proxied

An allowlist, never a denylist. An unlisted path returns 404 in Meta’s envelope. A denylist would mean every Graph endpoint Meta ships next month is exposed by default, including ones that mutate our own account structure — so new paths are opt-in rather than opt-out.

Six families are refused, each with a stated reason and a stated condition for re-entry, and the list is published rather than described: the coverage table has every path we proxy and every one we do not.

“Which parts of Graph do you support?” is the first question a technically serious buyer asks, and everything is a lie every proxy tells once. So the answer is a table.

Path validation

For every proxied request, the leading node ID in the path is checked rather than resolved:

  1. A phone-number ID must equal the one belonging to the key’s connection.
  2. A WABA ID must equal the account that connection belongs to.
  3. A media ID is forwarded with the phone-number ID forced to the key’s connection, so Meta itself enforces ownership.
  4. Any mismatch → 404 in Meta’s envelope, and nothing is sent to Meta.

404, not 403. A 403 would confirm that the ID exists somewhere in the system, which turns a guess into an oracle. The path never selects the connection; it is only ever checked against it.

The routing gate

Before anything is forwarded, the request is refused if the subscription does not permit routing. HTTP 403, in Meta’s envelope shape, so existing client error handling sees a familiar object:

{
  "error": {
    "code": "connection_not_routable",
    "reason": "billing_unpaid",
    "message": "The subscription does not currently permit API routing.",
    "docs": "https://cosend.app/docs/runtime-api#connection-not-routable"
  }
}

Connection not routable

This heading is the anchor the envelope above points at, which is why it is worded as the code is. reason is the field to branch on, and these seven are the whole set:

The seven reasons a connection is not routable, their causes and their fixes.
reasonCauseFix
billing_unpaidAn invoice failed; the subscription is past due or unpaidUpdate the card
subscription_canceledCancelled, or lapsedResubscribe
free_tier_message_capFree’s 1,000 routed messages a month is reached. The Runtime API itself is included on Free — this is the volume cap, not a feature gateUpgrade
quota_exceededA hard cap was reachedWait for the period, or upgrade
connection_suspendedWe suspended it, for abuse or an acceptable-use breachContact support
connection_not_activeThe connection needs reauthorisation, is disconnected, or is payment-blocked at MetaReconnect
test_key_on_live_connectionA test credential against a live connectionUse a live key

Headers

Which headers are forwarded to Meta, stripped, and returned to the caller.
DirectionHeaders
Forwarded to MetaContent-Type, Content-Length, Accept, Accept-Encoding, and Idempotency-Key if you sent one — which is also honoured locally
Stripped, alwaysAuthorization (replaced), Cookie, X-Forwarded-*, Host, and any X-Cosend-* header you send. A client must not be able to forge our internal headers
Returned to youMeta’s status code, Content-Type and body, plus X-Request-Id, the rate-limit headers and X-Cosend-Proxy. Meta’s own rate-limit headers are forwarded where present, prefixed X-Meta-

Streaming and limits

Request and response limits on the Runtime API.
ConcernBehaviour
Request bodyStreamed to Meta, not buffered. A 100 MB video upload must not live in an API process’s heap
Maximum upload100 MB, which is Meta’s own limit. Over it → 413 in Meta’s envelope
Response bodyStreamed back
Timeout30 seconds, connect plus read. On expiry → 504 in Meta’s envelope with upstream_timeout
RetriesNone on a non-idempotent method. A POST to /messages that times out is *in doubt*, not retried — a duplicate WhatsApp message to a real person is worse than an error. An idempotent GET retries once on a connection error

What gets recorded on the way past

Every proxied request is recorded asynchronously and never on the critical path. This is the part that makes a passthrough worth pointing at rather than merely equivalent:

  • A trace row — the host, never the full URL.
  • On a successful send: a message row with the returned message ID, and the contact and conversation rows created or touched. This is how a passthrough customer’s inbox fills up without them doing anything.
  • On a template list: the template mirror is refreshed.
  • A usage event for the send — counted for display and for limits, and never billed. Meta bills your WhatsApp Business Account directly and we do not mark it up.
  • On a connection in direct-delivery mode: the trace and the metadata only. No body is written at any point.

If the recording fails, the proxied response is still returned. Your send succeeded at Meta; our bookkeeping is not your problem.