Skip to main content

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 optional

instance_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_id with 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 header Idempotent-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 omit body, 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 management
  • POST /keys/{id}/rotate — rotate the API key while the old one stays alive for a grace period; see API key rotation
  • GET /conversations?instance_id= and GET /conversations/{jid}/messages — inbox
  • GET /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 guide
  • GET /stream — real-time SSE
  • GET /usage — usage telemetry
  • GET /me/entitlements · GET /me/subscription · GET /me/invoices — the account's plan, limits and invoices; see Billing
  • GET /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 to tracked_since instead 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​

WhatCap
Media sent from a URL (media_url)64 MiB
Campaign image upload5 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).