Graph-Compatible Runtime
The Runtime API is a deliberately allowlisted subset of Meta's WhatsApp Cloud API and WhatsApp Business Management API. For supported operations, migrate an existing Graph request by changing:
- https://graph.facebook.com/v25.0/...
+ https://api.dualhook.com/v25.0/...
- Authorization: Bearer <META_ACCESS_TOKEN>
+ Authorization: Bearer <DH_LIVE_KEY>
Keep the supported Meta path, method, query, and JSON shape. Dualhook is not an unrestricted Graph API proxy: only the operations in the Runtime API Endpoint Reference are accepted. When a request fails, Runtime API Errors works back from the response to the fix.
Connection-Scoped Keys
Each dh_live_... key is bound to exactly one Dualhook connection, organization, phone-number ID, and WABA ID. It cannot be used for another connection or as a general Meta credential.
Create and Store a Key
An organization admin creates the key from Connection → Overview → Outbound API key. Plaintext is displayed once. Dualhook stores only its SHA-256 hash.
Store the key in a server-side secret manager. Never put it in browser code, mobile applications, public automation exports, logs, screenshots, or support messages. Do not share, resell, sublicense, or reuse it for another customer.
Authorization: Bearer dh_live_...
Authentication Boundary
Customers submit only the dh_live_... credential. Never submit any of these values to the Runtime API:
- Meta access or app access tokens
- Meta App Secret or app ID
appsecret_proof- a second
Authorizationvalue - business, WABA, or phone IDs in a request body
- Graph hosts or webhook/callback URLs
Dualhook loads the selected connection's encrypted Meta authorization inside the Worker and applies Meta App Secret Proof internally to every authenticated Graph request. Caller credentials, proof, hosts, or asset overrides are rejected rather than forwarded.
Temporary media-download URLs are the documented exception: after a proof-aware media metadata request, the Worker downloads bytes from a validated Facebook/CDN hostname using Meta's Bearer-only media-download flow. Redirects remain manual.
Rotation
Rotation creates a new primary key and offers three choices for the old key:
- revoke immediately
- keep a one-hour grace overlap
- keep a 24-hour grace overlap
Use an overlap only long enough to deploy the new key.
Routing Holds and Revocation
A reversible billing, plan-limit, administrator, or Meta routing hold does not revoke the connection's runtime key. The key remains bound to the same connection but cannot route requests while the connection is blocked. If the final hold is released and Meta webhook routing is restored and verified, the same key works again.
Keys are invalidated by terminal connection removal, explicit key revocation or rotation, expiration of a grace key, or an administrator quarantining that credential after a security incident. Creating or rotating a key is unavailable while its connection is not routable.
connection_not_routable
A valid key receives HTTP 403 when its connection cannot currently route requests:
{
"error": {
"code": "connection_not_routable",
"reason": "billing_unpaid",
"message": "The subscription does not currently permit API routing.",
"docs": "https://dualhook.com/docs/runtime-api#connection_not_routable"
}
}
The reason is restricted to this public allowlist:
| Reason | Meaning |
|---|---|
onboarding_incomplete | Connection setup is incomplete. |
plan_limit_grace | A temporary plan-limit grace period applies. |
plan_limit_exceeded | The subscription's connection limit was exceeded. |
billing_unpaid | The subscription does not currently permit routing. |
subscription_canceled | The subscription was canceled. |
admin_hold_org | An administrator hold applies to the organization. |
admin_hold_connection | An administrator hold applies to this connection. |
meta_banned | Meta disabled or scheduled the account for disablement. |
meta_offboarded | Meta reports that the account is offboarded. |
meta_messaging_restricted | Meta restricted messaging for the connection. |
meta_token_invalid | Meta authorization is invalid or expired. |
meta_override_inactive | Required webhook routing is inactive. |
partner_removed | Meta reports that the Dualhook connection was removed. |
offboarding | Connection removal is in progress. |
connection_unavailable | No more specific public reason is safely available. |
Do not rotate a key in response to this error. Resolve the connection hold in Dualhook.
Other authorization responses:
401— missing, invalid, revoked, expired, or quarantined credential.403 asset_scope_conflict— the stored phone/WABA assets are also assigned to a conflicting active connection, so routing fails closed.403with an object-scope message — a media or template object is owned outside the selected connection.
Request Controls
The runtime enforces exact v25.0 routes, strict content types, explicit request schemas, field/query allowlists, bounded JSON, MIME-specific media limits, asset ownership, and manual redirects.
Raw Graph pagination URLs are never accepted or returned. Use the safe after or before cursor documented in the endpoint reference; Dualhook reconstructs the Graph request internally.
Every response includes X-Dualhook-Request-Id. Keep it in your content-free request log for support correlation.
Rate Limits
| Bucket | Default | Scope |
|---|---|---|
| Messages | 80 operations/second | connection |
| Media upload/delete | 20 operations/minute | connection |
| Media metadata/download | 120 operations/minute | connection |
| Phone/WABA health and status | 6 operations/minute per connection; 30/minute per WABA | connection and WABA |
| Management reads | 60 weighted units/minute | connection and WABA |
| Management mutations | 10 weighted units/minute | connection and WABA |
| Failed authentication | 20 rejected attempts/minute | credential |
Phone and WABA reads share a six-per-minute connection allowance and a
30-per-minute WABA allowance. Successful responses are cached per connection
for 60 seconds; X-Dualhook-Cache reports hit or miss, and cache hits
include an Age header in seconds. Other single-object management reads cost
1 unit; template and QR lists cost 2.
Profile, settings, and QR writes cost 2; a template header sample upload costs
4; template create/update costs 6; template deletion costs 10. One sample
upload followed by one create therefore fits the default ten-unit mutation
window.
After 30 failed Meta message requests within a rolling five-minute window, sending pauses for five minutes. Successful sends do not erase failures already accumulated in that window. A throttled response uses HTTP 429 and includes Retry-After.
Authentication is guarded separately before the database lookup. Once the database identifies a revoked or expired credential, that refusal is cached for up to 24 hours and continues returning 401. Other rejected credentials are throttled after 20 failed attempts within a minute: further requests receive HTTP 429 with Retry-After for five minutes. The throttle is keyed on the credential, not on your connection or IP address, so one dead key cannot slow down your other keys, and a key that authenticates successfully never accumulates against it. If you see this, stop retrying and check that the key is still active on the connection — a key revoked by connection cleanup or by a rotation cannot be revived, and a new one must be created on the connection's Overview page.
Privacy Model
Inbound customer-path webhooks remain direct from Meta to your endpoint. Outbound payloads and requested media transit Dualhook's Cloudflare Worker without body persistence or caching. Only successful, allowlisted phone/WABA metadata reads receive a connection-isolated 60-second response cache; no message, media, template, profile, or error response enters it.
Operational logs exclude request bodies, recipients, message content, media bytes, credentials, proofs, authorization headers, SIP credentials, encryption material, and raw Meta download or pagination URLs. Daily metrics contain only connection/organization scope, operation, status, latency, byte counts, safe Meta error metadata, and — on failed requests only — a truncated SHA-256 target fingerprint plus a failure reason and pipeline stage drawn from a fixed Dualhook vocabulary. The failure reason records why the runtime itself refused a request, never customer input, object IDs, or Meta message text; successful requests store no fingerprint, reason, or stage.