API Reference
The single source of truth is the OpenAPI spec (packages/sdk/openapi.yaml),
which generates the TypeScript SDK and this reference. Below are the core endpoints
with real examples. Authenticate with Authorization: Bearer bz_live_....
Send text — POST /messages/text
Only to and body are required. Don't pass a source number and bZapper picks one from your
pool (load spreading + conversation affinity):
curl -X POST https://api.bzapper.com.br/messages/text \
-H "Authorization: Bearer $BZ_KEY" -H "Content-Type: application/json" \
-d '{"to":"+5511988888888","body":"Hello","client_reference":"lead-42"}'
import requests
requests.post("https://api.bzapper.com.br/messages/text",
headers={"Authorization": f"Bearer {key}"},
json={"to": "+5511988888888", "body": "Hello"})
await fetch("https://api.bzapper.com.br/messages/text", {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ to: "+5511988888888", body: "Hello" }),
});
<?php
$ch = curl_init("https://api.bzapper.com.br/messages/text");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["to" => "+5511988888888", "body" => "Hello"]),
CURLOPT_RETURNTRANSFER => true,
]);
$res = curl_exec($ch);
Response 202: { "message_id": "...", "status": "queued", "client_reference": "lead-42" }.
instance_id is optionalinstance_id (and pool_id) are not required. Omit them and the gateway picks the number
(rotation/sticky). Pass instance_id only to force a specific number — e.g.
{"instance_id":"<id>","to":"...","body":"..."}. See List numbers
to get the ids. Behavior details in Support flows.
List numbers — GET /instances
Lists the project's instances (numbers), with the id you use as instance_id for targeted
sends. Requires the instances:read scope.
curl https://api.bzapper.com.br/instances -H "Authorization: Bearer $BZ_KEY"
{ "data": [
{ "id": "ce…", "phone": "+5511…", "nickname": "Support", "status": "connected", "health_score": 100 }
] }
In the admin, the Numbers screen shows each number's
instance_idwith a copy button.
Send idempotency
If you retry sends on your own (timeout, queue with retry), send the Idempotency-Key
header with a value unique per message (e.g. a UUID or the message id in your system). It
works on every POST /messages/{type} route:
curl -X POST https://api.bzapper.com.br/messages/text \
-H "Authorization: Bearer $BZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-4412-notice-1" \
-d '{"to":"+5511988888888","body":"Your order is out for delivery"}'
- The same key on the same account, for 24 hours, returns the same response (same
message_id) with the headerIdempotent-Replayed: true— the message is not sent again. - The first request is still running →
409 idempotency_in_progress: retry in a few seconds. - Same key with a different body →
422 idempotency_key_reused: use a new key per message. - The first request failed (any non-2xx answer) → the key is released and can be used again.
- Up to 255 characters (
400 idempotency_key_invalid). Without the header, nothing changes.
Other core endpoints
POST /messages/{image,video,document,audio,sticker,location,contact,poll,reaction,buttons,list}POST /messages/otp— verification code in 2 messages (text + the code bubble); counts as 1 send. The code is never persisted or shown in the inbox (masked + echoguard). If you omitbody, the API generates the text in the account's language. Details in Message types → OTP.POST /contacts/check—IsOnWhatsApp(handles@lid)POST /contacts/import·GET /contacts/export— bulk import (up to 1000 rows, upsert by phone,dry_run) and CSV export with the list's filters; see Contact managementPOST /keys/{id}/rotate— rotate the API key while the old one stays alive for a grace period; see API key rotationGET /conversations?instance_id=andGET /conversations/{jid}/messages— inboxGET /media/{id}— a stable reference to received media (private): responds with 302 to a fresh pre-signed URL. See Message types.POST /webhooks— register a webhook (HMAC); see the webhooks guideGET /stream— real-time SSEGET /usage— usage telemetryGET /me/entitlements·GET /me/subscription·GET /me/invoices— the account's plan, limits and invoices; see BillingGET /instances/{id}/uptime— number availability: % of time connected over 24h, 7d and 30d (warming counts as connected), since when it is in its current status, and the 20 latest status changes. History starts when it began to be recorded; a window shrinks totracked_sinceinstead of counting unrecorded time as offline.GET /ai-connections·DELETE /ai-connections/{id}— AI assistants (Claude, ChatGPT, Cursor…) connected to the account through the MCP connector with OAuth: an admin lists and revokes all of them; an agent, the ones they authorized. Revoking kills the tokens on the next call. The connector itself speaks the MCP protocol, not this API.
Errors
Every error has a stable neutral code plus a translated message:
{ "code": "not_connected", "message": "Number disconnected...", "locale": "en" }
Use the code in your logic. Common ones: unauthorized, forbidden,
rate_limited, not_connected, no_number_available, not_supported
(experimental).
402 free_quota_exceeded — the Free allowance
On the Free plan an account gets 150 interactions per month, and every message
counts as one: the ones you send and the ones you receive. Once the allowance is
used up, sends are refused with 402 free_quota_exceeded until the next cycle —
receiving keeps working (webhooks, inbox and history are unaffected). On Pro there
is no interaction allowance.
{
"code": "free_quota_exceeded",
"message": "Free plan allowance used up: 150 of 150 interactions this month (sent + received). Upgrade to Pro to send without limits — see Billing in the panel.",
"locale": "en",
"used": 250,
"included": 250,
"plan": "free"
}
used is this month's usage and included is the allowance. Your account's effective
values are in GET /me/entitlements; see Billing.
Rate limit
The limit is a token bucket per API key (per IP on unauthenticated routes).
The default is 20 requests per second with a burst of 40. Accounts with plan
enforcement enabled use their own plan ceiling (see GET /me/entitlements).
On overflow the API answers 429 with code: "rate_limited" and the header
Retry-After: 1 — the bucket refills in fractions of a second, and 1 s is
the smallest wait the header can express. There are no X-RateLimit-* headers.
Back off on the client; the official SDKs already honour Retry-After.
Media size
| What | Cap |
|---|---|
Media sent from a URL (media_url) | 64 MiB |
| Campaign image upload | 5 MB |
| Logo/avatar upload (account, project, white-label) | 5 MB |
Above the cap the API answers media_too_large. Stored media counts against
your plan allowance (100 MB on Free, 1 GB on Pro, +1 GB per add-on).