Errors, limits and idempotency
Every error of the WhatSetter API has the same shape and a stable code. This page lists them all, the headers that describe your limits, and what to do when a call fails.
The error envelope
Section titled “The error envelope”{ "error": { "code": "insufficient_scope", "message": "This API key is missing the \"messages:send\" scope." }}codeis stable: branch your code on it.messageis for humans and can change. Log it, do not parse it.- The HTTP status matches the code (see the table). Success responses use
{ "data": … }, and lists add{ "pagination": { "has_more", "next_cursor" } }.
Error codes
Section titled “Error codes”| HTTP | code |
When it happens | What to do |
|---|---|---|---|
| 400 | validation_error |
The path contains malformed URL encoding (for example %ZZ). |
Fix the URL. |
| 401 | missing_api_key |
No Authorization: Bearer header and no X-Api-Key header. |
Send the key. |
| 401 | invalid_api_key |
The key is unknown, revoked, or not in the ws_live_ format. |
Check the key, or create a new one. |
| 402 | plan_limit_reached |
POST /campaigns/{id}/resume when your plan’s pool of active campaigns is full. |
Pause another campaign, or upgrade. |
| 403 | insufficient_scope |
The key lacks the scope this endpoint needs. The message names it. | Create a key with that scope. |
| 404 | not_found |
Unknown route, or the resource does not exist, or it belongs to another workspace. | Check the id and the path. |
| 409 | invalid_state |
Pausing a campaign that is not active, resuming one that is not paused, adding a lead to a campaign that is not active, or handing over a lead that has no conversation yet. | Read the message and change the state in the dashboard. |
| 409 | lead_not_contacted |
POST /messages on a lead nobody has messaged yet. |
First contact goes through a list and a campaign. |
| 409 | request_in_flight |
A request with the same Idempotency-Key is still being processed. |
Wait a few seconds, then retry with the same key. |
| 409 | already_blocked |
POST /blocked-contacts for a number or prefix already in the list. |
Nothing: it is already blocked. |
| 422 | validation_error |
A parameter or a body field is missing or invalid: limit outside 1 to 100, a date that is not ISO 8601, an empty text, an unknown member id in assigned_to. |
Fix the request. Retrying as is will fail again. |
| 429 | rate_limited |
More requests per minute than your plan allows. | Wait Retry-After seconds, then retry. |
| 429 | quota_exceeded |
The daily WhatsApp quota of the sending number is reached (it resets at midnight UTC), or a workspace cap is reached: 200 lists, 10 webhook subscriptions, 500 blocked contacts. | Wait for X-Quota-Reset, or delete unused items. |
| 500 | internal_error |
An unexpected error on our side, or the messaging backend is not configured. | Retry later. Contact support if it persists. |
| 500 | server_error |
Agent control is not configured on this deployment (handoff, resume). |
Contact support. |
| 502 | send_failed |
The WhatsApp engine did not confirm the send. | Retry with the same Idempotency-Key. Without a key, read the conversation before sending again. |
| 502 | upstream_error |
The workflow behind handoff, resume or POST /campaigns/{id}/leads rejected the call or was unreachable. |
Retry in a moment. |
| 503 | agent_disconnected |
The campaign’s WhatsApp number is disconnected, or no number is attached to it. | Reconnect the number in the dashboard. |
| 503 | service_unavailable |
The authentication backend is unavailable. | Retry in a few seconds. |
Rate-limit headers
Section titled “Rate-limit headers”Requests are counted per workspace and per minute, all keys combined, with a limit set by your plan (60 to 1000 per minute, see Authentication and permissions). Every response carries:
X-RateLimit-Limit: 300X-RateLimit-Remaining: 287X-RateLimit-Reset: 2026-09-19T10:01:00.000ZWhen the limit is exceeded, the response is 429 rate_limited and adds Retry-After in seconds:
HTTP/1.1 429 Too Many RequestsRetry-After: 23Quota headers
Section titled “Quota headers”Sending messages has a second, separate budget: the daily quota of the WhatsApp number, the same one the campaign engine uses (see anti-ban protection). A successful POST /messages reports it:
X-Quota-Limit: 150X-Quota-Remaining: 149X-Quota-Reset: 2026-09-20T00:00:00.000ZThe same numbers are in the body under data.quota (limit, used, remaining, resets_at). When the quota is reached you get 429 quota_exceeded. It resets at midnight UTC. quota_exceeded is also used for the workspace caps listed in the table above.
Idempotency
Section titled “Idempotency”POST /messages is retry-safe when you send an Idempotency-Key header: any unique string of at most 200 characters, for example a UUID per send intent.
curl -X POST https://app.whatsetter.com/api/v1/messages \ -H "Authorization: Bearer $WHATSETTER_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4d3f9b2a-7c1e-4a8b-9f0d-2e6c8a1b3d5f" \ -d '{"lead_id":"66f1a2b3c4d5e6f7a8b9c0d1","text":"Are we still on for tomorrow at 2pm?"}'What the key does:
- A key that already produced a response replays it with the header
X-Idempotent-Replay: true. No second message is sent. - A retry fired while the first request is still running gets
409 request_in_flight. Wait a few seconds and retry with the same key. - If a send times out or answers
502 send_failed, retrying with the same key is safe: WhatSetter either replays the recorded result or answers409while it settles. It never sends twice. - Keys are scoped to your workspace and remembered for 15 minutes.
Version header
Section titled “Version header”Every response carries X-Api-Version: 2026-07, the version of the REST contract. Webhook payloads have their own api_version field (2026-06-01), described on the webhooks page.
Retry advice
Section titled “Retry advice”| Response | Retry? | How |
|---|---|---|
429 rate_limited |
Yes | Wait Retry-After seconds. |
429 quota_exceeded on POST /messages |
Yes, tomorrow | Wait for X-Quota-Reset (midnight UTC). |
502 send_failed, 502 upstream_error, 503 * |
Yes | Exponential backoff, starting at a few seconds. For POST /messages, reuse the same Idempotency-Key. |
500 internal_error |
Once, later | If it persists, contact support with the request path and time. |
409 request_in_flight |
Yes | Wait a few seconds, same Idempotency-Key. |
Any other 4xx |
No | The request itself is wrong. Fix it first. |

