Errors & idempotency
Branch on the error code, not the status. And make every retry that costs money safe.
Branch on the code, not the status
Errors come back as { "error": "<code>" }. The status alone is not enough to
decide what to do — two 403s can need opposite fixes.
| Status | Code | What it means, and what to do |
|---|---|---|
| 401 | invalid_key | Unknown or revoked key. Create a new one; retrying will never help. |
| 403 | ip_not_allowed | The key has an IP allowlist and this address is not on it. The key is fine — add the address at developers.wasync.app/keys. Do not rotate. |
| 403 | insufficient_scope | The credential lacks the scope this endpoint needs. |
| 403 | forbidden_portal_kind | POST /connections on a Bitrix24 portal. Connections there are created by the portal admin. |
| 400 | invalid_phone_number | phoneNumber must be digits only, no leading +. |
| 400 | invalid_webhook | The URL must be HTTPS on a publicly reachable host. |
| 402 | — | The connection's licence has lapsed. See Licences & trial. |
| 409 | already_connected | The session is live; there is nothing to pair. |
| 409 | not_supported_for_provider | A qr-only operation on a cloud_api number. Permanent — do not retry. |
| 429 | warmup_limited | Hourly warm-up cap on a new qr number. Back off in minutes. |
| 429 | rate_limited | Abuse limiter. Honour Retry-After when present; keep a default back-off regardless. |
| 502 | pairing_code_unavailable | An ordinary outcome, not an outage — fall back to the QR. |
Retry-After is not guaranteed on every 409/429, so never make your back-off
depend on it.
Idempotency on POST /connections
Creating a connection provisions a WhatsApp session and consumes a paid slot. A
client that times out and retries would otherwise create a second connection —
and you would silently pay for two slots for one customer. The optional
Idempotency-Key header closes that hole. It is supported on
POST /connections only; leave it off and behaviour is exactly as before.
A key is any opaque string up to 255 characters, namespaced to your workspace (two partners using the same literal string never collide), and it lives 24 hours from the first request that used it.
201 Created.200 OK plus the response header Idempotency-Replayed: true. The qr in that response is freshly fetched, not the code stored at first creation: pairing codes rotate roughly every 20 seconds, so a replayed stale QR would simply fail to scan. Treat 200 as success, not as an error.400 { "error": "idempotency_key_reuse" }. The comparison is a hash of the canonical body, so JSON key order does not matter.409 { "error": "idempotency_in_progress" } with Retry-After: 1. Honour it and retry with the same key.A key longer than 255 characters is rejected with
400 { "error": "idempotency_key_invalid" } — never truncated. A blank or
whitespace-only header counts as absent. The response body shape is unchanged in
all cases.
Generate one key per customer-onboarding attempt
A fresh UUID (crypto.randomUUID()) minted right before the call and reused
only by that call's retries. Do not use one key per process, per API key,
per day, or a constant. A key that coarse means the second customer you onboard
is handed the first customer's connection back instead of his own.
Idempotency on sends
POST /messages takes an idempotencyKey in the body (not a header). Always
send one — a UUID per logical send.
- On any error or timeout, retry with the same key. A new key means a second real WhatsApp message on someone's phone.
- On
409 in_progress, poll with the same key: honourRetry-Afterwhen present, otherwise back off about 5 s. - A slow send is not a failed send. Set your HTTP timeout to at least 180 s.