One error shape, a handful of status codes, and the two headers that make a failed call safe to send again.
Whatever went wrong, the body is the same object. error is prose for a human reading a log; code is the stable, machine-readable one.
Branch on code, never on the message text and never on the message alone - wording gets improved, codes do not.
{
"error": "bridge is not connected",
"code": "not_connected"
}
The codes are used narrowly, so each one points at a different fix.
400 - a missing or malformed field. Your payload is wrong; retrying unchanged will fail again.401 - missing or invalid bearer token. See API keys.402 - no subscription on the account, or payment past due. A human has to act in the dashboard.403 - the account bridge limit is reached.404 - no such bridge, message or contact.409 - conflicting state, such as pairing a bridge that is already paired.429 - over the rate limit. Wait for Retry-After.502 - WhatsApp itself rejected or failed the request.503 - the WhatsApp session is not connected. See stay connected.The split worth internalising: 4xx other than 429 means fix the request, 429 and 503 mean wait and repeat, 502 and 500 mean try again and alert if it persists.
A connection that dies mid-request leaves you unable to tell whether the send happened. Idempotency-Key removes the doubt: the first use performs the call and stores the response for 24 hours, and a repeat with the same key replays that stored response instead of sending again.
A replay is labelled, so you can tell the two apart in your own logs. A duplicate that arrives while the first is still in flight gets 409 idempotency_conflict - wait and retry rather than treating it as a failure. Only successful responses consume a key, so a call that failed can be retried with the same one.
curl -X POST https://wabridges.com/api/instances/user-123/proxy/send/text \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Idempotency-Key: order-4417-confirmation" \
-H "Content-Type: application/json" \
-d '{"chat": "15550001234", "body": "Your order shipped"}'
{
"message_id": "ACE41E988162AB19A4A7BB7A9E663693",
"timestamp": 1777180605
}
Derive the key from the thing you are doing - an order id, a notification row id - not from a fresh UUID per attempt. A key generated inside the retry loop is a new key every time and protects nothing.
Sending and reading allow 120 requests per minute; creating bridges allows 10. Over either, the response is 429 with a Retry-After header giving the seconds to wait.
Honour that header rather than guessing an interval. Retrying immediately keeps the window full and extends the block, and a fleet of workers all retrying on the same fixed timer will collide again on every cycle - spread them out.
Every response carries an X-Request-Id header. Log it next to your own request records, especially for failures.
If you send a well-formed one yourself - 1 to 64 characters of letters, digits, dashes or underscores - it is echoed back, so your id and ours are the same string and correlating the two sides is trivial. Anything else is replaced with a generated one. With that id, support can find the exact request instead of asking you three follow-up questions.
Something here not matching what you see? Write to us - a person answers.