Skip to main content
Every failure comes back in the same shape, whatever went wrong:
Branch your code on error.code and the HTTP status. error.message is written for a human reading a log and its wording can change without notice.
Some errors add a details object with extra machine-readable context — for example retry_after_seconds on a rate limit. Where that happens, it is noted below. Every response, success or failure, carries a request_id. Log it. It is the one thing that lets support find your exact request.

Handle these three first

Almost every integration only really needs to get three cases right:

409 instance_not_connected

Stop and alert a human. Retrying cannot help — somebody has to re-scan the QR. What to do

429 rate_limit_exceeded

Wait, then retry. The Retry-After header tells you how many seconds.

422 invalid_recipient

Skip this recipient. The number is not usable on WhatsApp. Do not retry it.

Full error reference

Authentication — 401

If you drive the instance endpoints from a signed-in portal session instead of a key, you may also see missing_session and invalid_session. Those never appear on the apikey path described here.

Billing and access — 402, 403

The request itself — 400, 404, 413, 422

Connection — 409

Rate limit — 429

Our side — 500, 502, 503

This page covers every error reachable through the public API described on this site. The portal has some additional administrative error codes that no API key can trigger; they are deliberately not listed here.

Telling the two number limits apart

instance_limit_reached covers two different situations, on purpose — to you both mean “you cannot add another number”. But the remedies differ, so details.limit_source tells you which one you hit:
Branch on the field, never on the wording
Never match on error.message to tell them apart. The wording can change; error.code and details.limit_source are the stable contract.

Rate limits

Sending is limited per minute, per API key.
  • One allowance is shared by every message type and the number check. It is one budget, not one per endpoint.
  • The instance endpoints (/v1/instances/...) have their own separate allowance of 120 requests per minute — comfortable for polling a QR every couple of seconds — so managing numbers never eats into your sending budget.

The headers

Any request that reaches a rate limiter carries all four of these — on success as well as on a 429:
Always read the scope before caching a limit. The two budgets are different sizes: managing numbers allows 120 a minute, sending usually far fewer. A client that reads X-RateLimit-Limit: 120 off a GET /v1/instances call and applies it to sending will overrun its real send budget and not understand why.
Read X-RateLimit-Limit rather than hardcoding a number — your send allowance comes from your plan and can change without any change to this API. Requests rejected before the limiter — a missing or invalid key, for instance — carry no X-RateLimit-* headers at all.

About X-RateLimit-Reset

The window is a rolling minute, not a fixed one that empties on the hour. There is no single instant when your count drops to zero, so X-RateLimit-Reset reports the useful thing instead: seconds until the oldest request ages out, which is when X-RateLimit-Remaining next goes up by one.
  • If X-RateLimit-Remaining is above 0, you can send now — you do not need to wait for the reset.
  • If it is 0, waiting X-RateLimit-Reset seconds gets you at least one more request.
  • On a 429, X-RateLimit-Reset and Retry-After are the same number.

A 429 body

Pacing a list

Do not fire your whole list at once and mop up the 429s afterwards. Space the sends out from the start — it is gentler on your allowance and much gentler on your WhatsApp number’s standing.
Node.js — respect the headers

What to retry, and what not to

Make retries idempotent on your side. This API has no idempotency key, so a retry after a network timeout can genuinely send the message twice. Record the message_id you get back and skip anything already recorded as sent.