Failure is the interesting part
Integrating a WhatsApp API is mostly handling the number that dropped its connection, the recipient who is not on WhatsApp, and the rate limit. Every failure carries a stable code so you never have to branch on prose.
The envelope
{
"error": {
"code": "session_not_connected",
"message": "Session 'support' is not connected.",
"detail": { "session": "support", "status": "logged_out" },
"request_id": "a1b2c3d4e5f6"
}
}Branch on code. The message is written for a human reading a log and changes with copy edits. detail carries field-level messages for a validation failure, and request_id is what turns a support conversation into a one-minute lookup — include it when you write in.
Codes worth handling
| Code | HTTP | What to do |
|---|---|---|
validation_error | 400 | A malformed request. detail holds the per-field messages. Fix and resend. |
authentication_failed | 401 | Missing, revoked or expired key. Not retryable. |
forbidden | 403 | The key is valid but lacks the scope, or is pinned to other sessions or IPs. |
not_found | 404 | No such resource in this organisation. |
session_not_connected | 409 | The number is not linked. Reconnect it — sending will not start working on its own if it was logged out. |
recipient_not_on_whatsapp | 422 | No WhatsApp account on that number. Never retry. Check in bulk with /v1/contacts/check/ first. |
quota_exceeded | 402 | A plan limit — numbers, seats, AI credits. Upgrade or free something up. |
sending_paused | 423 | The deliverability engine halted sending to protect the number. Not a rate limit; see below. |
throttled | 429 | Too many requests. Back off and retry; honour Retry-After. |
gateway_unavailable | 503 | The session gateway is unreachable. Your message was queued, not lost. |
423 is not 429
429 means you are asking too fast — back off and carry on. A 423 means the health engine judged the number to be at genuine risk of a ban and deliberately stopped sending. Retrying through it defeats the only mechanism protecting the number. Read /v1/sessions/{id}/health/, fix what the reasons say, and resume from the console.What is safe to retry
| Situation | Retry? |
|---|---|
| 429, 500, 502, 503, 504 | Yes — exponential backoff with jitter. |
gateway_unavailable | Not needed. The message is already queued and will go when the socket returns. |
session_not_connected | Only after the session reports connected again. |
sending_paused | No. Fix the underlying health problem. |
| 400, 401, 403, 404, 422 | No. These fail identically forever. |
The official SDKs retry 5xx, 429 and a disconnected session twice by default, with backoff. Every retry of a send should carry the same idempotency_key — on WhatsApp a duplicate is not a tidy-up job, the recipient has already been notified twice.
const wa = new WaveSenda({
apiKey: process.env.WAVESENDA_API_KEY!,
maxRetries: 3,
timeoutMs: 20_000,
})Rate limits
| Scope | Limit |
|---|---|
/v1/ | 600 requests per minute, per API key |
| Unauthenticated | 30 requests per minute, per IP |
| Login and register | 10 requests per minute |
| Per-key override | A lower ceiling can be set on any individual key |
These are API limits. They are not the limit on how fast a number may send — that is set by the sending policy and the warm-up plan, and it is a far smaller number. The API will happily accept sends that the deliverability engine then paces out over hours.
Pagination
Every list is cursor-paginated. Follow next until it is null rather than calculating offsets — the underlying data shifts while you page, and offsets silently skip rows.
{
"data": [ ... ],
"next": "https://api.wavesenda.com/api/v1/messages/?cursor=cD0yMDI2...",
"previous": null,
"limit": 50
}for message in wa.messages.iterate(session="support"):
process(message)