Skip to content
Start free

Errors & rate limits

The API uses conventional HTTP status codes and returns a JSON error 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.
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 seconds until the window resets.
Retry-After Seconds to wait before retrying — respect this.
X-Wabery-Limit-Type rate_limit or monthly_quota.

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