WASync · Developers

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.

StatusCodeWhat it means, and what to do
401invalid_keyUnknown or revoked key. Create a new one; retrying will never help.
403ip_not_allowedThe 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.
403insufficient_scopeThe credential lacks the scope this endpoint needs.
403forbidden_portal_kindPOST /connections on a Bitrix24 portal. Connections there are created by the portal admin.
400invalid_phone_numberphoneNumber must be digits only, no leading +.
400invalid_webhookThe URL must be HTTPS on a publicly reachable host.
402The connection's licence has lapsed. See Licences & trial.
409already_connectedThe session is live; there is nothing to pair.
409not_supported_for_providerA qr-only operation on a cloud_api number. Permanent — do not retry.
429warmup_limitedHourly warm-up cap on a new qr number. Back off in minutes.
429rate_limitedAbuse limiter. Honour Retry-After when present; keep a default back-off regardless.
502pairing_code_unavailableAn 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.

New key — the connection is provisioned as normal → 201 Created.
Same key, same body, within the TTL — the same connection is returned, nothing is provisioned again: 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.
Same key, different body, within the TTL400 { "error": "idempotency_key_reuse" }. The comparison is a hash of the canonical body, so JSON key order does not matter.
Same key, first request still in flight409 { "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: honour Retry-After when present, otherwise back off about 5 s.
  • A slow send is not a failed send. Set your HTTP timeout to at least 180 s.

On this page