Ir al contenido
Start free

Errors & rate limits

Esta página aún no está disponible en tu idioma.

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."
}
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.

A 404 or 405 can mean one of two different things:

  • The resource doesn’t exist — a genuine 404 from a real handler (e.g. an unknown flow id). These carry a JSON error body and a Wabery-Version response 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-Version header.

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.

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",
});

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.

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.

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);