Errors & rate limits
Este conteúdo não está disponível em sua língua ainda.
Errors use normal HTTP status codes and a JSON body:
{ "error": "contact_opt_in_required", "message": "An active WhatsApp opt-in record is required before sending to this contact."}Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
200 |
Success. |
400 |
Invalid request (missing/!malformed fields). |
401 |
Missing or invalid API key. |
403 |
Key lacks the required scope. |
404 |
Resource not found. |
409 |
Conflict or current-state block (e.g. duplicate idempotency key, outside the messaging window, contact opt-in required, template not approved). |
412 |
Channel/project readiness failed (e.g. WhatsApp publish_readiness blockers such as BUSINESS_NOT_VERIFIED or NO_PAYMENT_METHOD). |
422 |
Valid shape but semantically invalid for the operation. |
429 |
Rate limited — back off and retry. |
503 |
A required safety dependency is unavailable — honor Retry-After and retry with the same idempotency key. |
5xx |
Transient server error — safe to retry. |
Version skew & unavailable endpoints
Section titled “Version skew & unavailable endpoints”A 404 or 405 can mean one of two different things:
- The resource doesn’t exist — a genuine
404from a real handler (e.g. an unknown flow id). These carry a JSON error body and aWabery-Versionresponse header. - The endpoint isn’t served by the connected instance — the method/path
isn’t deployed (your SDK is newer than the API build). These come from the
framework/proxy, so they have no
Wabery-Versionheader.
The SDK distinguishes the two for you: a 404/405 with no Wabery-Version
header is raised as a WaberyEndpointNotAvailableError (a subclass of
WaberyApiError) with a message that names the method and path and points at the
likely capability gap — rather than a bare “request failed with HTTP 404”.
import { WaberyEndpointNotAvailableError } from "@wabery/sdk";
try { await wabery.flows.sendByConfigKey("lead_intake", { channelId, to });} catch (err) { if (err instanceof WaberyEndpointNotAvailableError) { // This build doesn't serve POST /flows/send. The list endpoints carry the // same data, so fall back to them (e.g. flows.list() / projects.list()). }}If you call the REST API directly, treat a 404/405 without a
Wabery-Version response header as “endpoint not available on this instance”
and fall back to the list endpoint (GET /flows, GET /projects) that exposes
the same data.
Retries & idempotency
Section titled “Retries & idempotency”Retry 429 and 5xx responses with exponential backoff. To make retries safe,
send an Idempotency-Key header on writes — Wabery returns the original result
for a repeated key instead of sending twice:
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", text: "Hi", idempotencyKey: "7c3f-order-2291",});curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Idempotency-Key: 7c3f-order-2291" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "text": "Hi" }'Rate limits
Section titled “Rate limits”Two limits apply per API key: a rate limit (default 600 requests / 60
seconds) and a monthly quota (default 100,000 requests / calendar month).
Both scale with your plan. Read the live values for your key from GET /limits:
const limits = await wabery.limits.retrieve();// { object: "public_api_limits",// rate_limit: { requests: 600, window_seconds: 60, scope: "api_key" },// monthly_quota: { requests: 100000, scope: "api_key", period: "2026-06" } }Exceeding either returns 429 with a JSON body and headers. X-Wabery-Limit-Type
tells you which limit tripped (rate_limit vs monthly_quota):
{ "error": "rate_limit", "message": "Public API rate limit exceeded", "limit": 600, "remaining": 0, "reset_at": "2026-06-20T14:22:00Z"}| Header | Meaning |
|---|---|
RateLimit-Limit |
The ceiling for the window that tripped. |
RateLimit-Remaining |
Requests left in the current window. |
RateLimit-Reset |
Unix timestamp when the window resets. |
Retry-After |
Seconds to wait before retrying — respect this. |
X-Wabery-Limit-Type |
rate_limit, monthly_quota, or message_pair_rate_limit. |
Message pair limit
Section titled “Message pair limit”POST /messages has an additional safety limit for individual messages:
one new individual message every 6 seconds for the same platform, provider sender, and
recipient. It applies to all currently send-capable channels: WhatsApp,
Instagram, and Messenger. Separate recipients and separate sender identities do
not share a bucket. WhatsApp group sends are excluded.
When the pair is cooling down, the API returns 429 with:
{ "error": "message_pair_rate_limit", "message": "Messaging to this recipient is rate limited. Retry after the indicated delay.", "channel_id": "channel_...", "channel_platform": "WHATSAPP", "queued": false, "retryable": true, "retry_after": 6}X-Wabery-Limit-Type is message_pair_rate_limit; RateLimit-Limit is 1;
and Retry-After, RateLimit-Reset, and the body’s retry_after describe the
remaining cooldown. Retry-After and retry_after are whole seconds, rounded
up so retrying at the advertised time is safe.
The rejected request is not queued. Wait for Retry-After, then repeat it with
the same Idempotency-Key. A replay of an already-recorded idempotency key
returns its existing result without consuming a new pair-limit slot.
Wabery enforces the same pair spacing at the final provider-send boundary for
messages created by the inbox, automations, and workers. These internal sends
are delayed in the queue rather than surfaced as an API 429.
If Wabery cannot reach the distributed limiter, it does not risk an unpaced
send. POST /messages returns 503 with:
{ "error": "message_rate_limit_unavailable", "message": "Messaging is temporarily unavailable because the delivery rate limiter could not reserve a safe send slot.", "queued": false, "retryable": true, "retry_after": 1}Honor Retry-After: 1 and retry with the same idempotency key. More send
examples are in Sending messages.
Pagination
Section titled “Pagination”List endpoints (contacts.list, conversations.list, conversations.listMessages,
templates.list, …) return a WaberyList:
{ "object": "list", "data": [ /* … */ ], "has_more": true }Page with limit and a starting_after cursor set to the last id you saw:
let startingAfter: string | undefined;do { const page = await wabery.contacts.list({ limit: 100, startingAfter }); for (const contact of page.data) process(contact); startingAfter = page.has_more ? page.data.at(-1)?.id : undefined;} while (startingAfter);Or let the SDK manage the cursor with the listAll() async iterator:
for await (const contact of wabery.contacts.listAll()) process(contact);