Skip to content

Errors

Every failure raises a subclass of WhagentError.

WhagentError
├── ValidationError        the library stopped the request locally (also a ValueError)
├── TransportError         no HTTP response at all — network failure, timeout
└── APIError               an error response from the API
    ├── AuthenticationError    401 / 190
    ├── InvalidRequestError    400 / 100, 131009
    │   └── MediaUploadError   400 / 131053
    ├── ForbiddenError         403 / 131005
    ├── NotFoundError          404
    │   └── MediaNotFoundError 404 / 100
    ├── RateLimitError         429 / 130429
    ├── NotDeliveredError      503 / 131016
    ├── PollReplacedError      409 / 1752041
    └── ServerError            5xx / 2
from whagent import ForbiddenError, RateLimitError, ValidationError

try:
    client.send_text(body)
except ValidationError as exc:
    ...          # never left the process: bad recipient, over a length cap, bad MIME type
except ForbiddenError:
    ...          # an agent may only message its creator
except RateLimitError as exc:
    ...          # already retried; exc.retry_after is set when the API sent one

What an APIError carries

Attribute
status HTTP status
code error.code — the value to branch on
message human-readable summary; usually begins with (#code), but the format is not guaranteed
details error.error_data.details, when the API supplied one; wording is not guaranteed
fbtrace_id include it in support requests
error_type always OAuthException, even for errors unrelated to authentication
raw the full error body

Branch on code, never on the text of message.

except APIError as exc:
    logger.error("send failed: code=%s status=%s trace=%s", exc.code, exc.status, exc.fbtrace_id)

Error code reference

error.code HTTP Meaning Exception
2 500 Internal server error — also returned for to="user:<id>" where the id is not a WhatsApp user ServerError
100 400 Token present but not valid; unknown media id; a length cap exceeded (then message is just the field name) InvalidRequestError
100 404 Media id unknown or expired when fetching its URL MediaNotFoundError
190 401 Authorization header absent or malformed AuthenticationError
130429 429 Too many requests RateLimitError
131005 403 Recipient is not the agent's creator ForbiddenError
131009 400 Required field missing or malformed; recipient not a WhatsApp user (including an agent: id); a reaction send; media id unknown or expired; marking read a message this agent did not receive; a non-integer poll parameter InvalidRequestError
131016 503 Not accepted for delivery NotDeliveredError
131053 400 Media rejected at upload — over the size limit, or an unaccepted MIME type MediaUploadError
1752041 409 A newer poll replaced this one PollReplacedError

An unknown code falls back to a class chosen by the HTTP status, so a code the platform adds later still raises something sensible.

Two traps worth knowing

A 401 and a 400 both mean "token". An absent or malformed Authorization header is 401 + code 190; a token that is present but invalid — rotated, revoked — is 400 + code 100. If sends suddenly fail with InvalidRequestError, check the token before the payload.

Code 100 is overloaded. It covers an invalid token, an unknown media id, and a length cap. details — or the field name in message — tells them apart.

Retries

The library retries automatically, with exponential backoff and jitter, up to max_retries (default 3):

Failure Retried? Why
429 ✅ Nothing was sent
503 code 131016 ✅ Explicitly "not accepted for delivery", so not sent
500 on a read or delete ✅ No side effect to duplicate
500 on POST /messages ❌ by default Unknown whether the message was sent
Connection reset / read timeout on a send ❌ by default Same
4xx ❌ A repeat fails identically
409 ❌ Means a second poller exists; retrying fights it
client = Client(token, retry_send_on_server_error=True)   # when a duplicate beats a loss
client = Client(token, max_retries=0)                     # do it all yourself

RateLimitError.retry_after carries the Retry-After header when the API sends one; the library honours it. Without one, the library backs off from 5 seconds (5, 10, 20, capped by backoff_max) rather than from backoff_base — the counters span 60 seconds, and a sub-second retry would only collect another 429.

See Sending messages → retrying a send for the full decision table.

Local validation

ValidationError means the request never left the process. It fires on a recipient that isn't user:<id>, a text body over 4096 characters, a caption over 1024 or on a type that doesn't take one, a reaction send, an unaccepted MIME type, a file over its size limit, an empty file, or a media send given both media_id= and file=.

Turn it off to see exactly what the API says:

client = Client(token, validate=False)