Official SDKs
Official bZapper libraries to integrate in minutes, in your language. They all cover the same set of API operations: the 13 message types (text, image, video, document, audio, sticker, location, contact, poll, reaction, buttons, list, and OTP), numbers/instances, API keys, usage, and the advanced features — groups, presence, conversations, and contacts.
There are 7 languages — Node/TypeScript, Python, PHP, .NET (C#), Java, Go and Ruby — and they all follow the Berni Software standard, the same one behind the bFocus SDKs:
- every API operation has a method (159), named after the spec's
operationId; - automatic, safe retries, because every write carries an
Idempotency-Keythe API honors (see Errors, retries and idempotency); - typed errors with a stable
codeand arequestIdfor support; - webhook signature verification (HMAC-SHA256) and the bZapper Connect client;
- one shared conformance suite that all 7 run — an endpoint without a method breaks the build;
- zero runtime dependencies (only Java brings Jackson).
Install with a single command — no cloning required. Node, Python, PHP, .NET, Java, Go and Ruby are already published (npm, PyPI, Packagist, NuGet, Maven Central, Go modules, and RubyGems).
The essentials: you only need your API key
The SDK already points at the production API (https://api.bzapper.com.br). You
pass no URL at all — just your API key (bz_live_..., generated in the panel
under API Keys). That's all.
The API URL is optional and only meant for development (
http://localhost:8080) or self-hosting. In production, leave it out.
Installation
| Language | Installation |
|---|---|
| Node / TypeScript | npm install @bzapper/client |
| Python | pip install bzapper |
| PHP | composer require bzapper/bzapper |
| Go | go get github.com/bernisoftware/bzapper-go@latest |
| .NET (C#) | dotnet add package Bzapper |
| Java (Maven) | see the <dependency> block below |
| Ruby | gem install bzapper (or gem "bzapper" in your Gemfile) |
For Node, Python, PHP, .NET, Go, and Ruby, the command above already downloads the latest version and all dependencies — you add nothing else.
Java — dependency (Maven / Gradle)
Add only the SDK artifact. The single runtime dependency (Jackson, for JSON) comes in transitively — you don't need to declare anything else.
Maven (pom.xml):
<dependency>
<groupId>br.com.bernisoftware</groupId>
<artifactId>bzapper</artifactId>
<version>0.16.0</version>
</dependency>
Gradle (build.gradle.kts):
implementation("br.com.bernisoftware:bzapper:0.16.0")
Requires Java 17+. No Gson, OkHttp, or any other manual lib — the SDK uses the
JDK's own java.net.http.HttpClient and pulls in Jackson by itself.
Quick start
Installed? Then just pass the API key and send. No URL.
Node / TypeScript
import { Bzapper } from '@bzapper/client';
const bz = new Bzapper({ apiKey: 'bz_live_...' });
await bz.sendText({ to: '+5511999999999', body: 'Hello from bZapper! 👋' });
Python
from bzapper import Client
bz = Client("bz_live_...")
bz.send_text(to="+5511999999999", body="Hello from bZapper! 👋")
PHP
use Bzapper\Client;
$bz = new Client("bz_live_...");
$bz->sendText("+5511999999999", "Hello from bZapper! 👋");
Go
bz := bzapper.NewClient("bz_live_...")
bz.SendText(context.Background(), bzapper.SendTextParams{
SendBase: bzapper.SendBase{To: "+5511999999999"},
Body: "Hello from bZapper! 👋",
})
Java
import com.bernisoftware.bzapper.BzapperClient;
import com.bernisoftware.bzapper.model.SendOptions;
var bz = new BzapperClient("bz_live_...");
bz.sendText(SendOptions.to("+5511999999999"), "Hello from bZapper! 👋");
.NET (C#)
using Bzapper;
using var bz = new BzapperClient("bz_live_...");
var msg = await bz.SendTextAsync(new SendText { To = "+5511999999999", Body = "Hello from bZapper! 👋" });
Ruby
require "bzapper"
client = Bzapper::Client.new("bz_live_...")
client.messages.send_text(to: "+5511999999999", body: "Hello from bZapper! 👋")
Point at dev/self-host (optional)
Only if you're not using production:
new Bzapper({ apiKey: 'bz_live_...', baseUrl: 'http://localhost:8080' }); // Node
Client("bz_live_...", "http://localhost:8080") # Python
new Client("bz_live_...", "http://localhost:8080"); // PHP
bzapper.NewClient("bz_live_...", bzapper.WithBaseURL("http://localhost:8080")) // Go
new BzapperClient("http://localhost:8080", "bz_live_..."); // Java
new BzapperClient("bz_live_...", new BzapperClientOptions { BaseUrl = "http://localhost:8080" }); // .NET
Bzapper::Client.new("bz_live_...", base_url: "http://localhost:8080") # Ruby
Tip: explore and test everything in the Playground inside the panel (admin), with real sends and ready-made code examples in each language.
Presence in a group
Showing "typing…" in a group is just pointing the presence at the group's JID:
bz.presence_chat(instance_id=inst, to="[email protected]", state="typing")
Webhooks — receiving and processing events
The SDKs receive the webhook payload and process it for you: they verify the
HMAC signature (X-Bzapper-Signature), turn the envelope into a typed event,
and route it to a per-type handler. Each SDK also does the CRUD for webhooks
(createWebhook/listWebhooks/…). Events:
message.{received,sent,delivered,read,failed},
instance.{connected,disconnected,banned,logged_out,warming,status},
group.{joined,participant_added,participant_removed,participant_promoted,participant_demoted,subject_changed,description_changed}.
Python
from bzapper.webhooks import Webhooks
hooks = Webhooks(secret="whsec_...") # secret returned by create_webhook
@hooks.on("message.received")
def _(event):
print(event.sender.name, event.payload["body"])
# in your endpoint — raw body + header. Raises SignatureError if invalid.
hooks.handle(raw_body=request.get_data(), signature=request.headers["X-Bzapper-Signature"])
Node / TypeScript
import { Webhooks } from '@bzapper/client';
const hooks = new Webhooks('whsec_...');
hooks.on('message.received', (e) => console.log(e.sender?.name, e.payload.body));
// Express: use express.raw() and the ready-made middleware
app.post('/webhooks', express.raw({ type: '*/*' }), hooks.middleware());
Go
rx := bzapper.NewWebhookReceiver("whsec_...").
On("message.received", func(e *bzapper.WebhookEvent) { /* ... */ })
http.Handle("/webhooks", rx) // it's an http.Handler: verifies + routes on its own
.NET (C#)
// body = the request's RAW bytes; invalid signature -> WebhookSignatureException
var ev = Webhooks.ConstructEvent(secret, body, req.Headers[Webhooks.SignatureHeader]);
if (ev.Type == "message.received") { /* ev.Sender, ev.Payload… */ }
Ruby
router = Bzapper::Webhook::Router.new("whsec_...")
router.on("message.received") { |ev| puts ev.sender&.dig("name"), ev.payload["body"] }
router.handle(request.body.read, request.get_header("HTTP_X_BZAPPER_SIGNATURE")) # verifies + routes; SignatureError if invalid
PHP (new Bzapper\Webhooks($secret)) and Java (new Webhooks(secret)) follow the
same pattern: on(type, handler) + handle(rawBody, signature). Use the
event_id for idempotency (the API may redeliver). Verification is timing-safe;
always pass the raw body (not the re-serialized JSON).
bZapper Connect (partner software)
If you are partner software and want your customers to subscribe to bZapper and
connect WhatsApp inside your product, use the partner client. It authenticates with
the bz_partner_… secret and only ever lives on the backend — never in a browser.
The walkthrough is in the Connect guide; every field is described in the reference.
Constructor
| SDK | Constructor |
|---|---|
| Node | new BzapperPartner({ partnerSecret, baseUrl?, locale?, timeout? }) or createPartnerClient({ … }) |
| Python | PartnerClient(partner_secret, base_url=None, locale=None, timeout=30) |
| Go | bzapper.NewPartnerClient(secret, opts...) (production) or bzapper.NewPartner(baseURL, secret, opts...) |
| PHP | new Bzapper\PartnerClient($partnerSecret, $baseUrl = null, $opts = []) |
| Java | new BzapperPartner(partnerSecret) or BzapperPartner.builder(partnerSecret)…build() |
| .NET | new PartnerClient(partnerKey) or new PartnerClient(partnerKey, new BzapperClientOptions { … }) |
| Ruby | Bzapper::PartnerClient.new(partner_key, base_url:, timeout:, max_retries:, locale:) (all optional except the key) |
Partner methods
| What it does | Node | Python | Go | PHP | Java | .NET | Ruby |
|---|---|---|---|---|---|---|---|
| Who am I | me() | me() | Me(ctx) | me() | me() | GetPartnerMeAsync() | get_partner_me |
| Open a session | createConnectSession({ external_id, customer, locale? }) | create_connect_session(external_id, customer, locale=None) | CreateConnectSession(ctx, CreateConnectSessionParams{…}) | createConnectSession($externalId, $customer, $locale = null) | createConnectSession(externalId, customer, locale) | CreateConnectSessionAsync(new CreateConnectSessionRequest { ExternalId, Customer, Locale }) | create_connect_session(external_id:, customer:, locale:) |
| Exchange the code | exchangeCode(code) | exchange_code(code) | ExchangeCode(ctx, code) | exchangeCode($code) | exchangeCode(code) | ExchangeConnectCodeAsync(new ExchangeConnectCodeRequest { Code }) | exchange_connect_code(code:) |
| List connections | listConnections({ external_id?, status? }) | list_connections(external_id=None, status=None) | ListConnections(ctx, ListConnectionsParams{…}) | listConnections($externalId = null, $status = null) | listConnections(externalId, status) | ListPartnerConnectionsAsync(externalId, status) | list_partner_connections(external_id:, status:) |
| One connection | getConnection(id) | get_connection(id) | GetConnection(ctx, id) | getConnection($id) | getConnection(id) | GetPartnerConnectionAsync(id) | get_partner_connection(id) |
| New key | rotateConnectionKey(id) | rotate_connection_key(id) | RotateConnectionKey(ctx, id) | rotateConnectionKey($id) | rotateConnectionKey(id) | RotatePartnerConnectionKeyAsync(id) | rotate_partner_connection_key(id) |
| End it | revokeConnection(id) | revoke_connection(id) | RevokeConnection(ctx, id) | revokeConnection($id) | revokeConnection(id) | RevokePartnerConnectionAsync(id) | revoke_partner_connection(id) |
List return shape: Node, Python, PHP, .NET and Ruby return the whole object, with the
{ data: [...] } envelope (in .NET, ListPartnerConnectionsResult.Data; in Ruby,
result["data"]), like the other list methods in those SDKs; Go and Java return the
unwrapped list ([]PartnerConnection / List<PartnerConnection>).
Customer-side methods (with the regular API key)
listConnectedApps() and revokeConnectedApp(id) — in Python, list_connected_apps()
and revoke_connected_app(connection_id); in Go, ListConnectedApps(ctx) and
RevokeConnectedApp(ctx, id); in .NET, ListConnectedAppsAsync() and
RevokeConnectedAppAsync(id); in Ruby, client.connect.list_connected_apps and
client.connect.revoke_connected_app(id). These are the account's "Connected apps": use them to show
your customer who is linked, and to disconnect.
Examples
import { BzapperPartner, createClient } from '@bzapper/client';
const partner = new BzapperPartner({ partnerSecret: process.env.BZAPPER_PARTNER_SECRET! });
// 1. your backend opens the session
const { session_token } = await partner.createConnectSession({
external_id: company.id,
customer: { name: company.owner, email: company.email, company: company.name, country: 'BR' },
});
// 2. the front end opens the component with that token and returns the `code`
// 3. your backend exchanges the code for the customer's key
const connection = await partner.exchangeCode(code);
await storeKey(company.id, connection.api_key);
// 4. from here on, it is the regular client with their key
const bz = createClient({ apiKey: await readKey(company.id) });
await bz.sendText({ to: '+5511999990000', body: 'Your order is on its way' });
from bzapper import PartnerClient, Bzapper, BzapperError
partner = PartnerClient(os.environ["BZAPPER_PARTNER_SECRET"])
session = partner.create_connect_session(
external_id=str(company.id),
customer={"name": company.owner, "email": company.email,
"company": company.name, "country": "BR"},
)
# … return session["session_token"] to the front end; after onComplete:
connection = partner.exchange_code(code)
store_key(company.id, connection["api_key"])
try:
Bzapper(api_key=read_key(company.id)).send_text(to="+5511999990000", body="Hi")
except BzapperError as e:
if e.code == "connect_suspended": # 402: the customer's Pro is unpaid
ask_customer_to_pay(company)
elif e.code == "connect_revoked": # 401: the customer disconnected you
delete_key(company.id)
partner := bzapper.NewPartnerClient(os.Getenv("BZAPPER_PARTNER_SECRET"))
session, err := partner.CreateConnectSession(ctx, bzapper.CreateConnectSessionParams{
ExternalID: company.ID,
Customer: bzapper.ConnectCustomer{Name: company.Owner, Email: company.Email, Company: company.Name, Country: "BR"},
})
// … after onComplete:
connection, err := partner.ExchangeCode(ctx, code)
storeKey(company.ID, connection.APIKey)
Partner webhooks
Events for all your connections arrive at a single endpoint, with the same
signature verification the SDKs already expose (Webhooks/verify), using the
partner webhook secret. The envelope carries an extra connection block:
- Node:
event.connection(plusisConnectEvent()andCONNECT_EVENT_TYPES). - Python:
event.connection(ConnectionRef) andCONNECT_EVENT_TYPES. - Go:
event.Connection(*WebhookConnection) andConnectEventTypes. - PHP: the event's
connectionkey and theWebhooks::EVENT_CONNECT_*constants. - Java:
event.connection()(WebhookConnection) andWebhooks.CONNECT_EVENT_TYPES. - .NET:
ev.Connection(WebhookConnection). - Ruby:
event.connection(aHash) andBzapper::Webhook::CONNECT_EVENT_TYPES.
Lifecycle events: connect.completed, connect.suspended, connect.resumed,
connect.revoked. Operational events (message.*, instance.*) arrive on the same
channel for active connections.
Codes your integration must handle
| Code | HTTP | What to do |
|---|---|---|
connect_suspended | 402 | The customer's Pro is unpaid: show a notice and reopen the component with the same external_id |
connect_revoked | 401 | The customer disconnected you: delete the stored key |
payment_pending | 409 | Previous payment being confirmed: wait and retry |
account_admin_required | 403 | The email has a bZapper account but is not an admin of it |
code_attempts_exceeded | 429 | Per-target code caps exceeded (5 sends / 10 attempts per hour) |
Official API and templates
The same SDKs, with no change of shape: sendText and the other sends work the same on both
rails. What only exists on the official API — templates, the
business account's numbers and the business profile — gets its own methods, named after the
operation in each language's convention.
| Convention | Node · PHP · Java | Python | Go | .NET | Ruby |
|---|---|---|---|---|---|
| Example | listTemplates() | list_templates() | ListTemplates(ctx) | ListTemplatesAsync() | client.templates.list_templates |
| Resource | Operations |
|---|---|
| Templates | listTemplates, createTemplate, getTemplate, deleteTemplate, syncTemplates, uploadTemplateMedia |
| Outside the 24h window | getFallbackTemplate, setFallbackTemplate |
| Sending a template | sendTemplate (in Ruby, client.messages.send_template) |
| Connection | getOfficialStatus, getOfficialSignupConfig, completeOfficialSignup, getOfficialAccount, connectOfficialAccount, disconnectOfficialAccount |
| Numbers | listOfficialNumbers, listAvailableOfficialNumbers, addOfficialNumber, setDefaultOfficialNumber, disconnectOfficialNumber |
| Business profile | getOfficialProfile, updateOfficialProfile, uploadOfficialProfilePhoto, reregisterOfficialProfile — and the per-number versions: getOfficialNumberProfile, updateOfficialNumberProfile, uploadOfficialNumberProfilePhoto, reregisterOfficialNumberProfile |
In Ruby, templates live on client.templates and the rest on client.official.
When the 24-hour window is closed and the project has a fallback template, a text send goes
out as that template and the result carries delivery (via, template, note)
describing the detour — in .NET MessageQueued.Delivery, in Go Message.Delivery, in Java
SentMessage.delivery().
const { numbers } = await bz.listOfficialNumbers();
const profile = await bz.getOfficialProfile();
await bz.updateOfficialProfile({ ...profile, about: 'Open 8am to 6pm' }); // send the whole profile
const r = await bz.sendText({ to: '+5511999999999', body: 'Your order is on its way.' });
if (r.delivery) console.log(r.delivery.note); // went out as a template, outside the window
Errors, retries and idempotency
All 7 SDKs behave the same way here — it is the Berni Software standard contract:
- Typed errors. Any non-2xx response throws an error from the
BzapperErrorfamily (BzapperExceptionin PHP, .NET and Java;*bzapper.Errorin Go;Bzapper::Errorin Ruby), with one subclass per status:AuthenticationError(401),PermissionDeniedError(403),NotFoundError(404),ConflictError(409),ValidationError(400/422),RateLimitError(429, withretryAfter),ServerError(5xx) andNetworkError(network failure/timeout, status0,codeNETWORK_ERROR). In PHP, .NET and Java the names end inException; in Go they are sentinels forerrors.Is(ErrNotFound,ErrRateLimit…). - Stable
code. Always branch on thecode— never on the text, which is localized (locale) and may change. requestIdon every error. Every call carries anX-Request-Id; the error exposes it (request_id/requestId/RequestId/getRequestId()). Send it to support: that is how we find your call in the logs.- Automatic retries (default 2, configurable;
0disables) only on network errors/timeouts,429,502,503and504— a500or a4xxreturns at once. They honorRetry-After(capped at 60 s) or use exponential backoff with jitter. - Safe thanks to idempotency. Every write (POST/PUT/PATCH/DELETE) carries an
Idempotency-Key, the same on every attempt, and the API honors it on every write: on a repeat it returns the original response (Idempotent-Replayed: true, for 24 h) instead of running it again. In short: the message is never sent twice.
Python
from bzapper import BzapperError, RateLimitError
try:
bz.send_text(to="+5511999999999", body="Hello!")
except RateLimitError as e:
time.sleep(e.retry_after or 1)
except BzapperError as e:
print(e.code, e.status_code, e.request_id) # e.g. "not_connected", 409, "a1b2…"
Node / TypeScript
import { BzapperError, RateLimitError } from '@bzapper/client';
try {
await bz.sendText({ to: '+5511999999999', body: 'Hello!' });
} catch (err) {
if (err instanceof RateLimitError) console.error(`wait ${err.retryAfter}s`);
else if (err instanceof BzapperError) console.error(err.code, err.status, err.requestId);
else throw err;
}
PHP
use Bzapper\BzapperException;
use Bzapper\RateLimitException;
try {
$bz->sendText('+5511999999999', 'Hello!');
} catch (RateLimitException $e) {
sleep($e->getRetryAfter() ?? 1);
} catch (BzapperException $e) {
echo $e->getErrorCode(), $e->getStatusCode(), $e->getRequestId(); // branch on the code, not the message
}
.NET (C#)
try
{
await bz.SendTextAsync(new SendText { To = "+5511999999999", Body = "Hello!" });
}
catch (RateLimitException e)
{
await Task.Delay(e.RetryAfter ?? TimeSpan.FromSeconds(5));
}
catch (BzapperException e)
{
logger.LogError("bZapper {Code} (HTTP {Status}, request_id {RequestId})", e.Code, e.Status, e.RequestId);
}
Java
try {
client.sendText(SendOptions.to("+5511999999999"), "Hello!");
} catch (RateLimitException e) {
retryLater(e.getRetryAfter()); // Duration
} catch (BzapperException e) {
log.error("bZapper {} ({}) request_id={}", e.getCode(), e.getStatusCode(), e.getRequestId());
}
Go
_, err := client.SendText(ctx, bzapper.SendTextParams{SendBase: bzapper.SendBase{To: "+5511999999999"}, Body: "Hello!"})
var e *bzapper.Error
if errors.Is(err, bzapper.ErrRateLimit) && errors.As(err, &e) {
time.Sleep(e.RetryAfter)
} else if errors.As(err, &e) {
log.Printf("%s (http %d) request_id=%s", e.Code, e.StatusCode, e.RequestID)
}
Ruby
begin
client.messages.send_text(to: "+5511999999999", body: "Hello!")
rescue Bzapper::RateLimitError => e
sleep(e.retry_after || 1)
rescue Bzapper::Error => e
warn "#{e.code} (HTTP #{e.status}) request_id=#{e.request_id}"
end
To also deduplicate re-runs of your code (a job that runs twice), pass your own key — for example, the order id:
bz.send_text(to="+5511999999999", body="Order 4471 confirmed", idempotency_key="order-4471") # Python
await bz.sendText({ to: '+5511999999999', body: 'Order 4471 confirmed' }, { idempotencyKey: 'order-4471' }); // Node
$bz->sendText('+5511999999999', 'Order 4471 confirmed', ['idempotency_key' => 'order-4471']); // PHP
await bz.SendTextAsync(new SendText { To = "+5511999999999", Body = "Order 4471 confirmed" },
new RequestOptions { IdempotencyKey = "order-4471" }); // .NET
client.sendText(SendOptions.to("+5511999999999").withIdempotencyKey("order-4471"), "Order 4471 confirmed"); // Java
ctx := bzapper.ContextWithIdempotencyKey(ctx, "order-4471") // Go: works for any write
client.messages.send_text(to: "+5511999999999", body: "Order 4471 confirmed", idempotency_key: "order-4471") # Ruby
Pin the exact SDK version (e.g.
bzapper==0.16.0,"@bzapper/client": "0.16.0",gem "bzapper", "0.16.0"): every release states whether it changes the public surface or is purely additive, so upgrading is your decision.
Each SDK has a full README (in the package's repository) with examples for every message type, groups, presence, conversations, and errors.