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, so 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. A workspace 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 range → 400 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.

Seventeen families are refused, and the list is published rather than described: the coverage table has every path we proxy and every one we do not. They are refused for two different reasons, and the difference is the one worth knowing: eleven are blocked and are not coming back, while six are real WhatsApp endpoints we have not proxied yet, each with a stated reason and a stated condition for re-entry.

“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.

Endpoint not available

404 in Meta’s envelope, with code: "endpoint_not_available". You reached this page from that error, so the reasoning is here rather than a link away.

The path you called is not on the allowlist. That is either because it is one of the seventeen families below, or because nothing has added it yet. An unlisted path is refused by default, and a denylist would have meant every Graph endpoint Meta ships next month was exposed until somebody noticed.

Eleven families are blocked, and the reason is the same for all of them: they mutate account structure, credentials or billing, and a key scoped to one customer must not be able to reach them. Several have a supported equivalent on /v1, which is the thing to reach for instead.

The eleven Graph path families the Runtime API blocks, and what to use instead.
BlockedWhy, and what to use
POST /{PNID}/deregisterMeta forbids it on coexistence numbers, and on cloud-API numbers it destroys the connection we hold open. Use DELETE /v1/connections/{id}
POST/DELETE /{WABA}/subscribed_appsUnsubscribing us from your webhooks would silently end every automation, inbox update and delivery receipt, and neither of us would know why. This subscription is ours
/oauth/access_token, /debug_tokenToken exchange and introspection: our app’s credentials, and our app ID and its true scopes
POST /{BUSINESS}/client_whatsapp_business_accountsCreates WABAs under our Business Portfolio
*/extended_credit_line_allocation_config, */credit_lines, */allocate_creditBilling and credit-line sharing. Real money
POST /{APP}/uploadsApp-scoped rather than customer-scoped. Template media goes to /{PNID}/media
*/system_users, */assigned_users, */business_usersBusiness-Manager administration
POST /{PNID}/smb_app_dataThe coexistence sync trigger: one-shot, with a 24-hour deadline. Fired by hand it consumes your only history sync
/{PNID}?fields=webhook_configuration (write)Webhook overrides are a /v1/connections setting, so our routing stays consistent with what we believe it to be
Everything on /{USER}, /me, /{PAGE}, ads, catalogs, InstagramNot WhatsApp

Six families are not proxied yet, which is a different statement: each is a real WhatsApp endpoint, excluded from v1 with a reason and a condition for re-entry rather than quietly omitted.

The six WhatsApp endpoint families not proxied in v1, why not, and what would bring each back.
Not yetWhy notComes back when
FlowsA stateful UI artefact with its own versioning and encryption; proxying it would hand you a Flow whose lifecycle we do not trackWe model Flows, and customers ask
CallingOn Meta’s coexistence exclusion list, so it would work only on cloud-API connections, a feature that silently does nothing for most customersMeta supports calling on coexistence
GroupsThe Groups GA and the coexistence exclusion list contradict each other; proxying an endpoint whose availability we cannot state is worse than not having itThe contradiction is resolved
Marketing MessagesSome accounts already route marketing sends differently, so this belongs behind our send endpoint as a routing decision rather than as a new surfaceThat routing lands
Product catalog and commerceOn Meta’s coexistence exclusion listCoexistence support
PaymentsRegional, regulated and moving. Money endpoints are the last thing to proxy blindlyExplicit legal review

If your path is in neither table, it is not blocked. It simply has not been added. New paths are opt-in by design, and the coverage table is where they appear when they are.

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 Free-plan ceiling or a connection limit was reached. Paid AI and automation usage has no ceiling: going over is billed, not refusedUpgrade, or add the connection to your plan
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.