Errors that tell you what to do
Every error is a JSON envelope with a stable code, a message that names the fix, and Retry-After on anything that clears by waiting. This page is the table your retry logic needs.
The shape
Retry-After: 42
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Daily send limit reached (20/day on Free). It clears at midnight, or Starter sends 60/day",
"messageKey": "serverErrors.dailyCapReached",
"details": { "scope": "day", "limit": 20, "resetAt": "2026-08-25T03:00:00.000Z", "retryAfterSecs": 42 }
}
}messageKey is stable, for your own translations. message is the English fallback. Refusals that need an upgrade name the cheapest plan that lifts them, because an error with no way forward is not an error message.
Every code
| 400 VALIDATION_ERROR | A field was wrong. `details` lists paths and rules, never your values. |
| 401 UNAUTHORIZED / INVALID_TOKEN | The key is missing, wrong, or revoked. Send `Authorization: Bearer wa_…`. |
| 402 PAYMENT_REQUIRED | A limit only a purchase lifts, like more numbers or batch sends. Waiting will not clear it. |
| 403 FORBIDDEN | The account is suspended. |
| 404 NOT_FOUND | No such resource, or it is not yours. |
| 409 NUMBER_NOT_CONNECTED | The number has no usable WhatsApp session. Open Numbers, check WhatsApp on the phone, then reconnect it. Retrying the send now will not help. |
| 409 CONFLICT | The request needs action or is already in that state. A restricted number needs `recoveryConfirmed: true`, only after you check that WhatsApp works on the phone. A request with the same `Idempotency-Key` may still be running: wait for `Retry-After`, then repeat it with the same key. |
| 422 IDEMPOTENCY_KEY_REUSED | This `Idempotency-Key` was already used for a different request. Send a new key. Waiting will not clear it. |
| 429 RATE_LIMIT_EXCEEDED | Too fast. `Retry-After` says exactly how long to wait. |
| 429 QUOTA_EXCEEDED | Daily cap, monthly quota, or a number's new-conversation budget. `details.scope` says which one: `day`, `month` or `newConversations`. `resetAt` says when it clears. |
| 429 PAIRING_COOLDOWN | Too many pairing-code requests for this number. `Retry-After` says when you can ask for another code. This is not a fixed restriction timer. |
| 502 GATEWAY_ERROR | WhatsApp rejected it upstream. Retrying might work. |
| 504 GATEWAY_TIMEOUT | WhatsApp did not confirm in time. Check the chat and, if present, watch for `details.messageId` on the `message.status` webhook before sending again. A retry with the same `Idempotency-Key` gets the same answer back without a second send. |
| 503 SERVICE_UNAVAILABLE | A dependency is down. Retry shortly. |
When your number is down
A phone can lose its connection, and a new account may have no number yet. New sends then answer 409 NUMBER_NOT_CONNECTED without a charge. Messages already queued that have not left pause until the number reconnects. They are not marked failed just because the number went offline, and they resume with the configured gap. message.failed is for a send that cannot proceed or whose outcome is unknown, not for a paused queue.
- Keep a second channel. Send what someone depends on by email too, or by email when WhatsApp refuses.
- Before you offer something that only works over WhatsApp, like a sign-in code, check that a number is connected. Keep the answer for a minute, and drop it when a send comes back
NUMBER_NOT_CONNECTED. - When no number is connected, hide that option and point to email instead. Don't show a button that can only fail.
- If you have a webhook,
number.disconnectedarrives when the number is still down after 15 minutes of retries, andnumber.connectedwhen it's back.
import { Whatsapi } from "@whatsapi.sh/sdk";
const wa = new Whatsapi({ apiKey: process.env.WHATSAPI_KEY! });
const numbers = await wa.listNumbers();
const whatsappReady = numbers.some((n) => n.status === "connected");Rules for retrying
Honor Retry-After on a 429. Never auto-retry a 402, because it clears by purchase, not by waiting. On a 504, do not blindly resend: the message id in details will appear on your message.status webhook if it landed, and resending without checking is how a person gets messaged twice. To retry a send safely, give it an Idempotency-Key header and repeat it with the same key: you get the first answer back instead of a second message. The rate limits are protection, not bureaucracy, and avoiding bans explains how to stay under them in the first place.