Skip to main content

bzapper

Official Python SDK for the bZapper WhatsApp gateway API — a multi-tenant WhatsApp gateway: connect numbers, send and receive messages, rotate numbers (anti-ban) and track usage.

Zero runtime dependencies (pure standard library). Python 3.9+.

Install

pip install bzapper

Hello world

from bzapper import Client

client = Client("bz_live_...")
client.send_text("+5511999999999", "Hello from bZapper!")

The base URL defaults to https://api.bzapper.com.br and is optional — pass one only for dev/self-host: Client("bz_live_...", "http://localhost:8080").

Client configuration

from bzapper import Client

client = Client(
    api_key="bz_live_...",                    # tenant API key
    base_url="http://localhost:8080",          # optional, defaults to prod (dev/self-host only)
    locale="pt-BR",                            # optional, sets Accept-Language
    timeout=30,                                # optional, seconds
)

Every request sends Authorization: Bearer <api_key>, Content-Type: application/json and, when locale is set, Accept-Language: <locale>.

Messages

Every message method accepts the common SendBase options as keyword arguments: instance_id, pool_id, quoted_message_id, quoted_participant (author of the quoted/reacted message — only needed in groups when it isn't in bZapper history), client_reference and mentions (JIDs or plain phones). Each returns the queued-message object (message_id, status, optional client_reference).

Safe retries: pass idempotency_key="..." (sent as the Idempotency-Key header, up to 255 chars) to any send. Repeating it within 24h returns the SAME response without sending twice (409 idempotency_in_progress while the first is still running; 422 idempotency_key_reused if the body changed).

client.send_text("+5511999999999", "Order #42 confirmed", idempotency_key="order-42")

to is a phone in E.164 (+5511999999999) or a JID.

# Text
client.send_text("+5511999999999", "Hello!")

# Image (use url OR base64, never both)
client.send_image("+5511999999999", {"url": "https://picsum.photos/600", "caption": "Hi"})

# Video
client.send_video("+5511999999999", {"url": "https://example.com/clip.mp4"})

# Document
client.send_document("+5511999999999", {"url": "https://example.com/file.pdf", "filename": "file.pdf"})

# Audio — set ptt=True for a voice note
client.send_audio("+5511999999999", {"url": "https://example.com/note.ogg", "ptt": True})

# Sticker
client.send_sticker("+5511999999999", {"url": "https://example.com/sticker.webp"})

# Location
client.send_location("+5511999999999", -23.5613, -46.6565, name="Av. Paulista", address="São Paulo")

# Contact
client.send_contact("+5511999999999", contact_name="Berni Software")

# Poll
client.send_poll("+5511999999999", "Pizza or sushi?", ["Pizza", "Sushi"], selectable_count=1)

# Reaction (quoted_message_id is the wa_message_id; empty emoji removes it)
client.send_reaction("+5511999999999", quoted_message_id="ABCD1234", emoji="👍")

# Buttons
client.send_buttons(
    "+5511999999999",
    "Choose an option:",
    [{"id": "a", "title": "Option A"}, {"id": "b", "title": "Option B"}],
    footer="Powered by bZapper",
)

# List
client.send_list(
    "+5511999999999",
    "Pick from the menu:",
    [{
        "title": "Drinks",
        "rows": [
            {"id": "1", "title": "Coffee", "description": "Hot"},
            {"id": "2", "title": "Tea"},
        ],
    }],
    button_text="Open menu",
)

MediaInput

The media argument is a dict: {"url"?, "base64"?, "caption"?, "filename"?, "mimetype"?, "ptt"?}. Use url OR base64, never both.

Caveat: buttons & lists

Buttons and lists are not reliable on WhatsApp (worse in groups). The API always also sends an equivalent numbered text menu as a fallback. Design your flows so the numbered menu alone is enough.

Instances (numbers)

client.list_instances()
inst = client.create_instance("+5511999999999", nickname="Support", proxy_url=None)
client.get_instance(inst["id"])

# Connect via QR (default) or pairing code
res = client.connect_instance(inst["id"], method="qr")    # -> {"status", "qr_code"?}
res = client.connect_instance(inst["id"], method="code")  # -> {"status", "pair_code"?}

client.disconnect_instance(inst["id"])

# Update the WhatsApp profile (display name / about / picture)
client.set_profile(inst["id"], display_name="Support", status_message="We reply fast")

Groups, presence and conversations

For these advanced calls instance_id is required. It travels in the query string for groups/conversations and in the body for presence/chats/contacts — the SDK handles that for you, you just pass it as an argument. jid is the group/chat JID.

inst_id = "01J..."  # an instance id

# Presence — works in groups too! Use the group JID as `to`.
client.presence_chat(inst_id, "+5511999999999", "typing")
client.presence_chat(inst_id, "12036304@g.us", "typing")    # group presence
client.presence_chat(inst_id, "12036304@g.us", "paused")

# Conversations
client.list_conversations(inst_id)
client.conversation_history(
    "12036304@g.us", inst_id, before="2026-06-01T00:00:00Z", limit=50  # limit ≤ 200
)

# Chats — archive / pin / mark read (on=True) or undo (on=False)
client.archive_chat("12036304@g.us", inst_id, on=True)
client.pin_chat("12036304@g.us", inst_id, on=True)
client.mark_chat("12036304@g.us", inst_id, on=True)

# Groups
client.list_groups(inst_id)
group = client.create_group(inst_id, "My group", ["+5511999999999", "+5511888888888"])
client.get_group(group["jid"], inst_id)
client.update_group_participants(
    group["jid"], inst_id, "add", ["+5511777777777"]  # add|remove|promote|demote
)
client.group_invite(group["jid"], inst_id)          # -> invite link/code
client.preview_group_invite(inst_id, "Cabc123InviteCode")  # name/size WITHOUT joining
client.join_group(inst_id, "Cabc123InviteCode")     # join via invite code
client.leave_group(group["jid"], inst_id)

# Contacts — which numbers are on WhatsApp?
client.contacts_check(inst_id, ["+5511999999999", "+5511888888888"])

Realtime (SSE)

The API exposes a server-sent-events stream at GET /stream for inbound messages and status updates. It is not wrapped by this SDK — connect with any SSE client, sending the same Authorization: Bearer <api_key> header.

API keys

client.list_keys()
created = client.create_key("CI key", role="agent")  # role: "admin" | "agent"
print(created["api_key"])  # raw key — shown only once, store it now
client.revoke_key(created["key"]["id"])

Usage

client.get_usage()  # whole period
client.get_usage(from_="2026-06-01T00:00:00Z", to="2026-06-30T23:59:59Z")  # RFC3339

Webhooks

Manage your webhook subscriptions:

hook = client.create_webhook(
    "https://yourapp.com/webhooks/bzapper",
    event_types=["message.received", "instance.banned"],  # omit = all events
)
print(hook["secret"])  # signing secret — returned ONCE, store it now
client.list_webhooks()
client.update_webhook(hook["id"], active=False)            # pause
client.update_webhook(hook["id"], secret="regenerate")     # rotate secret
client.delete_webhook(hook["id"])

Receive and process deliveries — bzapper.webhooks verifies the HMAC signature, parses the envelope into a typed event, and routes it to your handlers:

from bzapper.webhooks import Webhooks

hooks = Webhooks(secret="whsec_...")  # the secret from create_webhook

@hooks.on("message.received")
def _(event):
    print(event.sender.name, event.payload.get("body"))

@hooks.on("instance.banned")
def _(event):
    alert(event.instance_id)

# In your HTTP endpoint (framework-agnostic). Pass the RAW body bytes and the
# X-Bzapper-Signature header. Raises SignatureError if the signature is invalid.
hooks.handle(raw_body=request.get_data(), signature=request.headers["X-Bzapper-Signature"])

The typed event has id, type, timestamp, instance_id, client_reference, group, sender, mentions, payload and the original raw dict. Use event.id for idempotency (the API may retry deliveries). For lower-level use there's verify_webhook(secret, body, signature) and construct_webhook_event(secret, body, signature).

bZapper Connect

bZapper Connect lets a partner software let ITS customers subscribe to bZapper Pro and connect WhatsApp without leaving the partner's product. The partner receives an API key authorized by the customer; the customer stays a direct bZapper account.

The partner backend authenticates with the partner secret (bz_partner_..., sent as Authorization: Bearer bz_partner_...). Never send it to a browser.

The flow:

  1. Your backend creates a session: create_connect_session() → session_token (30 min).
  2. Your front end opens the embedded component with that token (BzapperConnect.open({ session })).
  3. When the customer finishes (Pro paid + WhatsApp connected), the component emits bzapper:complete with a one-time code (valid 10 min).
  4. Your backend exchanges it: exchange_code(code) → the customer's api_key (bz_live_..., shown once — store it).
  5. Your backend uses the regular Client(api_key) for that customer.

Partner client

from bzapper import PartnerClient

partner = PartnerClient("bz_partner_...")  # base_url / locale / timeout optional, like Client

partner.me()  # who the secret belongs to

session = partner.create_connect_session(
    "customer-42",                        # external_id: YOUR id; same id = same connection
    {
        "name": "Ana Souza",              # name OR company is required
        "email": "ana@boxy.com",          # required
        "phone": "+5511988887777",        # optional, pre-fills the number
        "company": "Boxy Pharma",         # optional, becomes the account/project name
        "country": "BR",                  # optional, sets the currency
    },
    locale="pt-BR",
)
session["session_token"], session["expires_at"], session["connection"]

conn = partner.exchange_code("cc_91ab...")  # -> connection + "api_key" (shown once)

partner.list_connections()                                    # -> {"data": [...]}
partner.list_connections(external_id="customer-42", status="active")
partner.get_connection(conn["id"])
partner.rotate_connection_key(conn["id"])  # new api_key; the previous one stops working
partner.revoke_connection(conn["id"])      # 204; does NOT cancel the customer's plan

Connection status: pending_account, pending_payment, pending_number, active, suspended, revoked.

With the customer's key, two error codes are specific to Connect:

code HTTP Meaning
connect_suspended 402 The customer's Pro is unpaid. Resumes by itself once paid (connect.resumed).
connect_revoked 401 The connection ended (by the customer, by you, or account deletion).

Partner webhooks

Your partner webhook is signed with the same HMAC scheme (X-Bzapper-Signature: sha256=<hex> over the raw body), using your partner webhook secret — so Webhooks works unchanged. The envelope is the regular one plus connection (event.connection: id, external_id, account_id, project_id, status). Besides project events of active connections (messages, number status…), you receive the lifecycle events connect.completed, connect.suspended, connect.resumed and connect.revoked (also in bzapper.webhooks.CONNECT_EVENT_TYPES).

Complete backend example (Flask)

import os

from flask import Flask, abort, jsonify, request

from bzapper import BzapperError, Client, PartnerClient
from bzapper.webhooks import SignatureError, Webhooks

app = Flask(__name__)
partner = PartnerClient(os.environ["BZAPPER_PARTNER_SECRET"])
hooks = Webhooks(secret=os.environ["BZAPPER_PARTNER_WEBHOOK_SECRET"])


# 1) Your front end calls this, then opens BzapperConnect with the token.
@app.post("/whatsapp/connect-session")
def connect_session():
    user = current_user()  # your own auth
    s = partner.create_connect_session(
        external_id=str(user.id),
        customer={"name": user.name, "email": user.email, "company": user.company},
        locale="pt-BR",
    )
    return jsonify(session=s["session_token"])


# 2) The component emitted `bzapper:complete` with a code: exchange it on the backend.
@app.post("/whatsapp/connect-complete")
def connect_complete():
    code = request.get_json()["code"]
    try:
        conn = partner.exchange_code(code)
    except BzapperError as err:
        return jsonify(error=err.code), err.status_code
    save_customer_key(conn["external_id"], conn["id"], conn["api_key"])  # store it (encrypted)
    return jsonify(status=conn["status"])


# 3) Use the customer's key with the regular client.
@app.post("/whatsapp/send")
def send():
    user = current_user()
    client = Client(load_customer_key(user.id))
    try:
        res = client.send_text(request.get_json()["to"], request.get_json()["body"])
    except BzapperError as err:
        if err.code == "connect_suspended":   # 402: customer's Pro unpaid
            return jsonify(error="Your WhatsApp plan is unpaid. Update the payment to resume."), 402
        if err.code == "connect_revoked":     # 401: connection ended
            forget_customer_key(user.id)
            return jsonify(error="WhatsApp disconnected. Connect again."), 409
        raise
    return jsonify(res)


# 4) Partner webhook: verify the signature, then react to connect.* events.
@hooks.on("connect.completed")
def _(event):
    mark_whatsapp(event.connection.external_id, "active")

@hooks.on("connect.suspended")
def _(event):
    mark_whatsapp(event.connection.external_id, "suspended")  # show "payment pending"

@hooks.on("connect.resumed")
def _(event):
    mark_whatsapp(event.connection.external_id, "active")

@hooks.on("connect.revoked")
def _(event):
    forget_customer_key(event.connection.external_id)

@hooks.on("message.received")
def _(event):
    route_inbound(event.connection.external_id, event.payload)


@app.post("/webhooks/bzapper")
def bzapper_webhook():
    try:
        event = hooks.handle(
            raw_body=request.get_data(),  # RAW bytes, never re-serialized JSON
            signature=request.headers.get("X-Bzapper-Signature"),
        )
    except SignatureError:
        abort(400)
    # event.id is stable: skip duplicates, the API retries failed deliveries.
    return "", 204

Connected apps (customer side)

With the customer's own key, list and disconnect partner apps using the account's WhatsApp:

client.list_connected_apps()            # -> {"data": [{"id", "partner_name", "status", ...}]}
client.revoke_connected_app("conn_id")  # admin; the partner's key stops working immediately

Error handling

Non-2xx responses raise BzapperError with a stable code, a localized message and the status_code. Always branch on code — never parse the human-readable message.

from bzapper import BzapperError

try:
    client.send_text("+5511999999999", "Hi")
except BzapperError as err:
    if err.code == "instance_not_connected":
        # reconnect flow...
        ...
    elif err.code == "rate_limited":
        # back off...
        ...
    else:
        print(err.code, err.status_code, err.message)

Example

A runnable script is in examples/quickstart.py.

License

MIT © Berni Software

Metadata

Release files for bzapper 0.6.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bzapper 0.6.2
File Size Uploaded
bzapper-0.6.2.tar.gz 33.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bzapper 0.6.2
File Interpreter ABI Platform
bzapper-0.6.2-py3-none-any.whl Python 3 none any Details

Total release size: 59.4 kB

Release files / bzapper-0.6.2.tar.gz

Download URL bzapper-0.6.2.tar.gz
Size 33.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c86832555125352bbe54aec0337f4d0b6ea7ebf9bc9d193c923b436feaca2bea
BLAKE2b-256 checksum
How to use checksums
fa3de7765187e0de4f44eed1888be67359c06ca4f2a2dcd7b68c751784c861de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / bzapper-0.6.2-py3-none-any.whl

Download URL bzapper-0.6.2-py3-none-any.whl
Size 25.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6f0024acb8d5484375b7e3aafe4b64e24f560e905d43658e90f903d584b9c839
BLAKE2b-256 checksum
How to use checksums
d5e1f23e337c84831f673aacf4b4b57fbd06993b9dd74dd1ed50c4f403689fbf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release history Release notifications | RSS feed

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

This release

0.6.2 This release

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page