Skip to main content

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-Key the API honors (see Errors, retries and idempotency);
  • typed errors with a stable code and a requestId for 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).
Available in the official registries 🎉

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​

LanguageInstallation
Node / TypeScriptnpm install @bzapper/client
Pythonpip install bzapper
PHPcomposer require bzapper/bzapper
Gogo get github.com/bernisoftware/bzapper-go@latest
.NET (C#)dotnet add package Bzapper
Java (Maven)see the <dependency> block below
Rubygem 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.8.1</version>
</dependency>

Gradle (build.gradle.kts):

implementation("br.com.bernisoftware:bzapper:0.8.1")

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​

SDKConstructor
Nodenew BzapperPartner({ partnerSecret, baseUrl?, locale?, timeout? }) or createPartnerClient({ … })
PythonPartnerClient(partner_secret, base_url=None, locale=None, timeout=30)
Gobzapper.NewPartnerClient(secret, opts...) (production) or bzapper.NewPartner(baseURL, secret, opts...)
PHPnew Bzapper\PartnerClient($partnerSecret, $baseUrl = null, $opts = [])
Javanew BzapperPartner(partnerSecret) or BzapperPartner.builder(partnerSecret)…build()
.NETnew PartnerClient(partnerKey) or new PartnerClient(partnerKey, new BzapperClientOptions { … })
RubyBzapper::PartnerClient.new(partner_key, base_url:, timeout:, max_retries:, locale:) (all optional except the key)

Partner methods​

What it doesNodePythonGoPHPJava.NETRuby
Who am Ime()me()Me(ctx)me()me()GetPartnerMeAsync()get_partner_me
Open a sessioncreateConnectSession({ 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 codeexchangeCode(code)exchange_code(code)ExchangeCode(ctx, code)exchangeCode($code)exchangeCode(code)ExchangeConnectCodeAsync(new ExchangeConnectCodeRequest { Code })exchange_connect_code(code:)
List connectionslistConnections({ 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 connectiongetConnection(id)get_connection(id)GetConnection(ctx, id)getConnection($id)getConnection(id)GetPartnerConnectionAsync(id)get_partner_connection(id)
New keyrotateConnectionKey(id)rotate_connection_key(id)RotateConnectionKey(ctx, id)rotateConnectionKey($id)rotateConnectionKey(id)RotatePartnerConnectionKeyAsync(id)rotate_partner_connection_key(id)
End itrevokeConnection(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 (plus isConnectEvent() and CONNECT_EVENT_TYPES).
  • Python: event.connection (ConnectionRef) and CONNECT_EVENT_TYPES.
  • Go: event.Connection (*WebhookConnection) and ConnectEventTypes.
  • PHP: the event's connection key and the Webhooks::EVENT_CONNECT_* constants.
  • Java: event.connection() (WebhookConnection) and Webhooks.CONNECT_EVENT_TYPES.
  • .NET: ev.Connection (WebhookConnection).
  • Ruby: event.connection (a Hash) and Bzapper::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​

CodeHTTPWhat to do
connect_suspended402The customer's Pro is unpaid: show a notice and reopen the component with the same external_id
connect_revoked401The customer disconnected you: delete the stored key
payment_pending409Previous payment being confirmed: wait and retry
account_admin_required403The email has a bZapper account but is not an admin of it
code_attempts_exceeded429Per-target code caps exceeded (5 sends / 10 attempts per hour)

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 BzapperError family (BzapperException in PHP, .NET and Java; *bzapper.Error in Go; Bzapper::Error in Ruby), with one subclass per status: AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), ConflictError (409), ValidationError (400/422), RateLimitError (429, with retryAfter), ServerError (5xx) and NetworkError (network failure/timeout, status 0, code NETWORK_ERROR). In PHP, .NET and Java the names end in Exception; in Go they are sentinels for errors.Is (ErrNotFound, ErrRateLimit…).
  • Stable code. Always branch on the code — never on the text, which is localized (locale) and may change.
  • requestId on every error. Every call carries an X-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; 0 disables) only on network errors/timeouts, 429, 502, 503 and 504 — a 500 or a 4xx returns at once. They honor Retry-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.8.1, "@bzapper/client": "0.8.1", gem "bzapper", "0.8.1"): 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.