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 Billing
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).
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).