bZapper Connect
bZapper Connect is for partner software: CRMs, ERPs, customer-service platforms and vertical systems that want to offer WhatsApp to their own customers.
Your customer clicks "Turn on WhatsApp" inside your product. The bZapper component opens, already filled with their details. Right there they:
- create the account on bZapper (no password, no captcha, no email confirmation — they are already signed in to your product);
- subscribe to bZapper Pro (card or Pix);
- connect the number (QR code or pairing code).
At the end, your backend receives an API key for the customer's account and starts sending and receiving messages with it. The customer never leaves your screen.
Your customer becomes a direct bZapper customer: the subscription, invoices and data are theirs. You get a credential they authorized, which they can disconnect at any time under Connected apps. You never see a card or a password.
Flow overview
Your backend Your front end (browser) bZapper
│ │ │
│ 1. POST /partner/connect-sessions (bz_partner_…) ─────────▶│
│◀──────────────── session_token (cs_…, 30 min) ─────────────│
│── session_token ───────────▶│ │
│ │ 2. BzapperConnect.open(...) ─▶│ account → Pro → number
│ │◀── onComplete({ code }) ──────│
│◀────────── code ────────────│ │
│ 3. POST /partner/connect/exchange { code } ───────────────▶│
│◀──────────────── api_key (bz_live_…) + connection ─────────│
│ 4. use the api_key as usual (send, receive, webhooks) │
│◀═══════════ connect.* + message.* webhooks ════════════════│
| Credential | Lives in | Used to |
|---|---|---|
bz_partner_… (partner secret) | your backend only | open sessions, exchange the code, manage connections |
cs_… (session token) | browser, 30 min | open the component |
cc_… (completion code) | browser → your backend, 10 min, single use | exchange for the API key |
bz_live_… (customer API key) | your backend only | operate the customer's WhatsApp |
0. Partner registration
Partners are registered by the bZapper team. You provide:
- name and logo (shown to the customer on the authorization screen). The logo is uploaded as a file by the bZapper team — PNG, JPEG, WebP or SVG, up to 2 MB — and is then served from bZapper's CDN, so it never breaks when you change your own site;
- allowed origins: the domains where the component runs (e.g.
https://app.yourproduct.com). Anything else gets403 origin_not_allowed; - webhook URL that receives the events of all your connections.
You receive the partner secret (bz_partner_…) and the webhook secret. Both are
shown only once.
1. Open the session (your backend)
When the customer clicks "Turn on WhatsApp", your backend calls:
curl -X POST https://api.bzapper.com.br/partner/connect-sessions \
-H "Authorization: Bearer $BZAPPER_PARTNER_SECRET" \
-H "Content-Type: application/json" \
-d '{
"external_id": "customer-4821",
"customer": {
"name": "Ana Souza",
"email": "[email protected]",
"company": "Boxy Pharma",
"phone": "+5511988887777",
"country": "BR",
"locale": "pt-BR"
}
}'
{
"session_token": "cs_8a82868d9a91…",
"expires_at": "2026-09-17T17:20:00Z",
"connection": { "id": "7ece7f98-…", "external_id": "customer-4821", "status": "pending_account" }
}
external_idis the customer id in your system. The sameexternal_idalways reuses the same connection. Calling it again after completion opens the component in manage mode.customer.companybecomes the account and project name on bZapper ("Boxy Pharma"). Without a company, we use the person's name.customer.phoneis pre-filled on the WhatsApp step.customer.countrysets the currency (BR → BRL; Americas → USD; others → EUR). Pix only shows up in BRL.
Only the session_token goes to the front end. It lasts 30 minutes and only opens the
component from your registered origins.
What if the customer already has a bZapper account?
The component always asks "Do you already have a bZapper account?" — Create new account or I have an account. If the email you sent already has an account, the screen opens straight on I have an account, with "We found your account". The customer can also link using another email (their bZapper account's email, if it differs from the one in your system).
To link, the person goes through one extra step: a 6-digit code we send to the account's email, and that email must be an admin of the account. Without it, any system that knew someone's email could get access to that person's account. After the code, the flow is the same (if the account is already Pro, payment is skipped).
2. Open the component (your front end)
Load the script once:
<script src="https://widget.bzapper.com.br/v1/connect.js"></script>
Modal (recommended)
async function turnOnWhatsApp() {
const { session_token } = await fetch('/api/bzapper/session', { method: 'POST' }).then((r) => r.json());
BzapperConnect.open({
session: session_token,
onComplete: async ({ code }) => {
await fetch('/api/bzapper/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
},
onClose: () => console.log('closed'),
onError: ({ code }) => console.warn('bZapper Connect:', code),
});
}
BzapperConnect.open() returns { close() } if you need to close it from code.
Inline
<bzapper-connect data-session="cs_8a82868d9a91…"></bzapper-connect>
<script>
document.querySelector('bzapper-connect')
.addEventListener('bzapper:complete', (e) => sendToBackend(e.detail.code));
</script>
Options and events
Option (open) | Attribute (inline) | Description |
|---|---|---|
session | data-session | Required. The session_token from step 1. |
locale | data-locale | Language (pt, en, es, it, de, fr). Default: <html lang> or the browser's. |
apiBase | data-api | API base. Default: https://api.bzapper.com.br. |
| Callback | DOM event | detail |
|---|---|---|
onReady | bzapper:ready | { step, connectionId, status } |
onStep | bzapper:step | { step }: account, verify_email, payment, number, manage, revoked |
onComplete | bzapper:complete | { code, connectionId, externalId } |
onClose | bzapper:close | — (modal only) |
onError | bzapper:error | { code, message } (e.g. connect_session_expired) |
The component uses Shadow DOM, so your site's CSS does not affect it (and its CSS does not affect your site). The card form is Stripe's own; card numbers never go through your code or ours.
If your site uses a Content-Security-Policy, allow:
script-src https://widget.bzapper.com.br https://js.stripe.com;
connect-src https://api.bzapper.com.br;
frame-src https://js.stripe.com https://hooks.stripe.com;
3. Exchange the code for the API key (your backend)
curl -X POST https://api.bzapper.com.br/partner/connect/exchange \
-H "Authorization: Bearer $BZAPPER_PARTNER_SECRET" \
-H "Content-Type: application/json" \
-d '{ "code": "cc_4291a95ab388…" }'
{
"id": "7ece7f98-…",
"external_id": "customer-4821",
"status": "active",
"account_id": "08eaa5be-…",
"project_id": "88a27b8c- …",
"api_key": "bz_live_45cd5a…",
"numbers": [{ "id": "209bd3cd-…", "phone": "+5511988887777", "status": "connected" }]
}
Store the api_key with your customer (external_id). It is not shown again. The
code works once and for 10 minutes.
You get the connect.completed webhook anyway. Call
POST /partner/connections/{id}/rotate-key to issue a new one (the previous key stops
working immediately).
4. Use the API key
It is a regular bZapper API key, bound to the connection's project. It covers
everything that operates their WhatsApp: messages, numbers, groups,
conversations, presence, labels, calls, pools and campaigns — plus POST /contacts/check
(does this number have WhatsApp).
Three limits are deliberate:
- Project, not account. Numbers in the customer's other projects do not exist for
that key (
404), even though the owner is the same. - Nothing about the account or money. Plan and billing, users, other API keys,
account webhooks, projects and account deletion answer
403 forbidden. - No
GET /stream, no account address book. The SSE feed is filtered per account and carries the QR and pairing codes; the contact base (/contacts,/tags,/contact-groups,/suppressions,/blocklist) is account-wide too. You get events through your webhook, already filtered per connection.
Connection lifecycle
status | Meaning | The key |
|---|---|---|
pending_account | Session open, account not created yet | — |
pending_payment | Account created, Pro not paid | — |
pending_number | Pro paid, WhatsApp not connected | — |
active | Completed | works |
suspended | The customer's Pro is unpaid | answers 402 connect_suspended |
revoked | Ended (by the customer, by you, or account deletion) | answers 401 connect_revoked |
A connection only exists while paid. The partner channel has no Free plan. If the
Pro renewal fails, the connection becomes suspended and goes back to active by
itself as soon as the customer pays. To let them pay, just open the component again
(same external_id): it goes straight to the payment screen, with a notice.
When the customer disconnects you under Connected apps, the connection becomes
revoked and releases the account: the key dies and any call on an open session
answers 409 connection_revoked. Opening the component again with the same
external_id restarts at the account step — with the code sent to the customer's
email. You cannot reconnect on your own; whoever disconnected has to authorize again.
Handling suspension in your code
const res = await fetch('https://api.bzapper.com.br/messages/text', {
method: 'POST',
headers: { Authorization: `Bearer ${customer.bzapperKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ to: '+5511999990000', body: 'Your order is on its way' }),
});
if (res.status === 402) {
const { code } = await res.json();
if (code === 'connect_suspended') showFixWhatsAppBanner(); // reopens Connect
}
Partner webhooks
All events of your connections arrive at the partner webhook URL, with the same
signature as bZapper webhooks (X-Bzapper-Signature: sha256=<hex>, HMAC-SHA256 of
the raw body with the partner webhook secret). It is the usual envelope plus a
connection block so you know which customer it belongs to:
{
"event_id": "evt_3f…",
"event_type": "message.received",
"timestamp": "2026-09-17T16:55:02Z",
"instance_id": "209bd3cd-…",
"payload": { "type": "text", "from": "+5511999990000", "body": "Hi!" },
"connection": {
"id": "7ece7f98-…",
"external_id": "customer-4821",
"account_id": "08eaa5be-…",
"project_id": "88a27b8c-…",
"status": "active"
}
}
Lifecycle:
| Event | When |
|---|---|
connect.completed | The customer finished (Pro paid + WhatsApp connected) |
connect.suspended | The customer's Pro is no longer paid |
connect.resumed | The customer paid and the connection is back |
connect.revoked | The connection ended. payload.revoked_by: customer, partner or account_deleted |
Operations (only for active connections): the same bZapper project events, such
as message.*, instance.* and contact.opted_out. QR codes and pairing codes are
not forwarded: they are the secret of the customer's device.
No webhook needs to be registered on the customer's account. Retries: up to 5, with
exponential backoff. To reconcile, use GET /partner/connections.
Managing connections (backend)
| Method | Route | Purpose |
|---|---|---|
GET | /partner/me | Who you are (checks the secret) |
GET | /partner/connections?external_id=&status= | List connections |
GET | /partner/connections/{id} | Status, account, project and numbers |
POST | /partner/connections/{id}/rotate-key | New API key (the previous one dies) |
DELETE | /partner/connections/{id} | End the connection (does not cancel the customer's plan) |
On the customer side: Connected apps
In the bZapper dashboard, under Connected apps, the customer sees the software linked
to their account and can disconnect any of them. The partner key stops right away and
you receive connect.revoked. The plan and numbers stay with the customer.
An account created through Connect has no password. If the customer wants to open the dashboard directly (invoices, cards), they use the welcome link sent by email or "Forgot password" on the login screen.
Every endpoint, field, state and error code is in Reference — bZapper Connect.
SDKs
All 5 official SDKs include the partner client. See SDKs.
| SDK | Partner client |
|---|---|
| Node | new BzapperPartner({ partnerSecret }) |
| Python | PartnerClient(partner_secret) |
| Go | bzapper.NewPartnerClient(secret) |
| PHP | new Bzapper\PartnerClient($secret) |
| Java | BzapperPartner |
Errors
| Code | HTTP | Where | What to do |
|---|---|---|---|
partner_unauthorized | 401 | /partner/* | Missing, wrong or rotated secret |
partner_inactive | 403 | all | Integration disabled by bZapper |
external_id_required / customer_email_required / customer_name_required | 400 | create session | Fill in the data |
origin_not_allowed | 403 | component | The page's domain is not a registered origin |
connect_session_expired | 401 | component | Open a new session |
invalid_code | 400 | exchange | Wrong, expired (10 min) or used code → use rotate-key |
connection_revoked | 409 | component | The customer disconnected you: open a new session (it restarts at the account step) |
payment_pending | 409 | payment | The previous attempt is being confirmed (Pix/3DS). Wait a few seconds |
account_admin_required | 403 | existing account | The email has a bZapper account but is not an admin of it |
code_attempts_exceeded | 429 | existing account | Too many wrong attempts for that email in the last hour |
code_sends_exceeded | 429 | existing account | Too many codes sent to that email in the last hour |
account_not_found | 404 | existing account | "I have an account" with an email that has no bZapper account |
connection_not_active | 409 | rotate-key | Connection not completed or ended |
connect_suspended | 402 | API key | Customer's Pro unpaid → reopen Connect |
connect_revoked | 401 | API key | Connection ended → reopen Connect to reconnect |