The Dualhook Platform API returns stable, typed error codes with a consistent JSON shape. Every error includes a code (a stable machine-readable string), a human-readable message, and a type you can group on. Codes like waba_webhook_conflict and idempotency_key_collision are the ones you'll most often need to handle explicitly.
Error shape
{
"error": {
"code": "invalid_redirect_url",
"message": "successRedirectUrl must match allowed_redirect_urls in partner settings",
"type": "invalid_request"
}
}
Error codes
| Code | When |
|---|---|
unauthorized | Missing or invalid bearer token |
forbidden | API key not bound to a Platform-tier subscription |
subscription_inactive | Subscription is unpaid/canceled |
invalid_request | Body validation failed |
invalid_redirect_url | URL not in your allowed_redirect_urls whitelist |
invalid_webhook_url | Webhook URL did not pass SSRF validation (must point to a public address) |
idempotency_key_collision | Same Idempotency-Key, different body, within 1h |
webhook_preflight_failed | Session creation only: the simulated hub.challenge GET against your webhookOverrideUrl failed. The error includes preflight.category (the errorCategory taxonomy from onboarding.failed, plus two preflight-only values: endpoint_unreachable for DNS/TLS/connection failures and endpoint_http_error for unexpected non-2xx statuses), preflight.httpStatus, and a receivedPreview of the body. Pass skipWebhookPreflight: true if your endpoint allowlists Meta's IPs/user-agent |
waba_webhook_conflict | A connection on the same WABA belonging to a different tenant has a different webhook URL and/or verify token (same-tenant re-onboarding replaces the config instead — see onboarding sessions). The error context includes mismatch (url | verify_token | both) and the existing URL on file |
webhook_subscribe_failed | Meta rejected the new webhook URL on PATCH |
meta_token_expired | The stored Cloud API access token has been revoked or expired and must be reconnected |
meta_error | Meta returned an error we couldn't map to a more specific code; check metaErrorCode / metaTraceId if present |
connection_suspended | Connection is currently not eligible for secret reveal |
event_not_redeliverable | POST /events/:id/redeliver on an event that is pending (still retrying) or already delivered |
not_found | Connection / session / tenant not found |
rate_limited | Per-scope limit exceeded — see Rate limits |
internal_error | Server-side issue — safe to retry with the same Idempotency-Key |
Error type buckets
The type field is a coarser bucket the code belongs to. Useful for client-side
error handling that doesn't need to switch on every code:
| Type | When |
|---|---|
authentication | unauthorized (401) |
authorization | forbidden, subscription_inactive (402/403) |
invalid_request | All 4xx except auth + rate limits |
rate_limit | rate_limited (429) |
server | internal_error, meta_error (5xx) |
Related
- Onboarding sessions —
idempotency_key_collision,invalid_redirect_url,waba_webhook_conflict - Connections —
webhook_subscribe_failed,not_found - Rate limits
- Platform API overview