Errors & rate limits
Conventional HTTP status codes, a stable machine-readable error code on every failure, and a per-key rate limit with standard headers.
Error format
Failures return a JSON body with an error.code you can branch on and a human-readable message.
json
{
"error": {
"code": "window_closed",
"message": "The 24h window is closed — send an approved template instead"
}
}Status codes
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The request body or query is wrong — the message says which field. |
| 401 | unauthenticated / invalid_api_key | Missing, malformed, or revoked key. |
| 403 | missing_scope | The key lacks the scope this endpoint requires. |
| 404 | template_not_found / not_found | Unknown template, message, contact, automation, run, or private link. |
| 409 | window_closed | Free-form text sent outside the 24h window — send a template. |
| 409 | opted_out | The contact has opted out of messages. |
| 422 | invalid_phone | The phone number is not valid E.164. |
| 402 / 403 | subscription_inactive / quota_exceeded | Plan issue — check billing. |
| 429 | rate_limited | Too many requests — slow down. |
| 500 | internal_error | An unexpected server error. Safe to retry with backoff. |
| 5xx | — | WhatsApp/Meta upstream error. Safe to retry with backoff. |
Automation errors
Starting an automation — with POST /automations/:id/enroll or a private link — returns 409 when the automation or your workspace’s guardrails stop it. The code says which one, and whether a retry can ever succeed.
| Code | Meaning | Retry? |
|---|---|---|
not_published | The automation isn’t live. | After you publish it |
no_trigger | The automation has no active trigger. For a private link: its trigger is no longer “From another app”. | After you fix the trigger |
concurrent | The contact is already part-way through this automation. | After their current run ends |
reentry | The contact has already been through it. | Never |
opted_out | The contact unsubscribed. | Never |
lane | Another automation in the same group is already running for them. | Later |
quota | Your plan’s automation run limit is used up. | After an upgrade or the next billing period |
contact_loop | This contact was started too many times recently. | Later, not straight away |
Retry guidance
- Retry
429after theRetry-Afterseconds, and5xxwith exponential backoff. - Don’t blindly retry
4xx— fix the request instead. - For automation
409s, follow the Retry? column —reentryandopted_outwill never succeed.
Sends are not idempotent
A retry after a timeout may double-send. When in doubt, check
GET /messages/:id (or the inbox) before resending.Rate limiting
Each key is limited to 120 requests per minute (fixed window). Every response includes:
http
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 22 # seconds until the window resetsOn 429, honor Retry-After. For large syncs, batch and spread requests out — contact upserts are cheap but still count against the limit.