Public API
Errors
Every error response shares the Meta Graph API body shape, so existing Cloud API clients keep their error handlers intact when migrating.
Body shape
{
"error": {
"message": "Invalid or expired API key",
"type": "OAuthException",
"code": 190,
"error_subcode": 460,
"error_data": { "details": "..." },
"fbtrace_id": "abc123"
}
}message— human-readable description (English)type—OAuthExceptionfor auth-class errors,GraphMethodExceptionfor everything elsecode— machine-readable error code (see table below)error_subcode— optional finer-grained codeerror_data— optional structured payload (e.g. for recipient-blacklist rejections)fbtrace_id— echoes the request'sX-Request-Idheader when present, for cross-system correlation
HTTP status drives client behavior
Treat the HTTP status as the source of truth for retry / backoff decisions; the Meta error
code is for observability and human troubleshooting. Multiple Meta codes can map to the same HTTP status.Common codes
| Code | Type | HTTP | Meaning |
|---|---|---|---|
| 190 | OAuthException | 401 | Invalid token — key is wrong, revoked, expired, HMAC failed, or the path resource does not exist / is outside the key's organizations |
| 200 | OAuthException | 403 | Permission denied — key valid but missing scope, IP rejected, or the requested organization is outside the key's authorization |
| 4 | OAuthException | 429 | Rate limit exceeded — backoff using the Retry-After header |
| 100 | GraphMethodException | 400 / 404 / 409 | Parameter error, resource not found, or conflict (e.g. tag limit reached) |
| 131000 | GraphMethodException | 400 / 409 / 422 / 500 | Generic send / processing failure. HTTP status disambiguates: 422 = blacklist hit, 409 = idempotency in-flight or resource limit (e.g. contact tag cap), 500 = downstream Meta or transient. |
| 131026 | GraphMethodException | 400 | 24-hour customer service window expired — send via a template instead |
Organization targeting errors
Admin keys (sflo_admin_) add three failure modes that organization keys never hit:
400— missing organization. The endpoint has no:phoneNumberId/:wabaIdto derive from and noX-SigFollow-Tenant-Idheader was sent.400— contradicting organization. The header names one organization while the path resource belongs to another. Fix whichever is wrong; SigFollow will not guess.403— organization not accessible. The organization exists but is outside this key's authorization. Call GET /v1/tenantsto see the current set—it may have changed if the issuing administrator's permissions changed.
Unauthorized path resources return 401, not 403
When the organization is derived from
:phoneNumberId or :wabaId, a resource outside your authorization returns the same 401as a resource that does not exist. This is deliberate: Meta's IDs are enumerable, and a distinguishable response would turn any key into a probe for other customers' phone numbers. Do not treat this 401 as "bad token"—check the resource ID first.Retry guidance
- 5xx and 429 are retryable. Use exponential backoff (e.g. 1s, 2s, 4s, 8s) capped at 30 seconds. Combine with an Idempotency-Key to make retries safe for write endpoints.
- 4xx (excluding 429) is notretryable — the request is malformed or the resource doesn't exist. Fix the request before retrying.
- Network errors (TLS, DNS, connection reset) before a status is received are retryable with the same idempotency key.
The recipient_blacklisted error
When you send with ?filter_blacklist=true and the target is on the tenant blacklist, the API returns 422 with a Meta-style body whose error_data.reason is recipient_blacklisted. No message row is created and Meta is not called. Treat this as a final state, not a retry target.
{
"error": {
"message": "Recipient is on the tenant blacklist",
"type": "GraphMethodException",
"code": 131000,
"error_data": { "reason": "recipient_blacklisted" }
}
}