Reference — bZapper Connect
This page is the complete technical reference for Connect. For the step-by-step walkthrough, start with the bZapper Connect guide.
API base: https://api.bzapper.com.br · Component: https://widget.bzapper.com.br/v1/connect.js
1. Mental model
Connect ties three parties together, each with its own credential:
| Party | Credential | Lives in | Reach |
|---|---|---|---|
| Your backend | bz_partner_… (partner secret) | your server only | open sessions, exchange the code, manage your connections |
| The component in the customer's browser | cs_… (session ticket, 30 min) | the browser, on your page | operate one connection: account, payment, number |
| Your backend, operating WhatsApp | bz_live_… (customer API key) | your server only | WhatsApp of the connection's project |
The durable entity is the connection: one customer of yours (external_id) linked to
a bZapper account. It is unique per (partner, external_id) — the same external_id
always resolves to the same connection, which makes
POST /partner/connect-sessions idempotent.
The code (cc_…) exists only to cross the browser without carrying the API key: it is
single use, valid for 10 minutes, and only the partner that owns the connection
can exchange it.
2. Authentication
| Surface | How it authenticates | Notes |
|---|---|---|
/partner/* | Authorization: Bearer bz_partner_… | Header only. The secret is not accepted in a query string. |
/connect/* | X-Connect-Session: cs_… | On GET /connect/stream (SSE) ?session=cs_… also works, because EventSource cannot send headers. Every other route rejects the query form. |
Customer API (/messages, /instances…) | Authorization: Bearer bz_live_… | The key returned by the code exchange. |
On top of the credential, /connect/* checks the Origin header against the partner's
registered origins (exact match, no wildcard; case and trailing slash are
normalized). Anything else: 403 origin_not_allowed.
The origin allowlist protects the customer from another page opening the component.
It is not a boundary against you, the partner: Origin is an ordinary header that
any server-side call can set. What authenticates /connect/* is the cs_… ticket —
treat it as a secret and keep it out of logs.
3. Partner endpoints (/partner/*)
GET /partner/me
Validates the secret and returns your registration.
{ "id": "…", "slug": "bfocus", "name": "bFocus", "logo_url": "https://…",
"allowed_origins": ["https://app.bfocus.com.br"],
"webhook_url": "https://api.bfocus.com.br/webhooks/bzapper",
"key_scopes": ["instances:read","instances:write","messages:send","contacts:check","presence:write","media:read"] }
POST /partner/connect-sessions
Opens (or reuses) your customer's connection and issues the component ticket.
| Field | Type | Required | Description |
|---|---|---|---|
external_id | string (≤200) | yes | The customer id in your system. Use a stable id, never rotated. |
customer.name | string | name or company | The person's name. |
customer.email | string | yes | Decides the path (new account × existing account). |
customer.company | string | — | Becomes the account and project name on bZapper. |
customer.phone | string (E.164) | — | Pre-fills the WhatsApp step. |
customer.country | string (ISO-3166 alpha-2) | — | Sets the currency: BR→BRL, Americas→USD, others→EUR. Pix is BRL only. |
customer.locale | string (BCP-47) | — | Language of the component and of the emails. |
{ "session_token": "cs_8a82…", "expires_at": "2026-09-17T17:20:00Z",
"connection": { "id": "7ece…", "external_id": "customer-4821", "status": "pending_account" } }
Errors: 400 external_id_required, 400 customer_email_required,
400 customer_name_required, 401 partner_unauthorized, 403 partner_inactive.
While the account is not linked, each new session refreshes the prefill. Once linked, the data belongs to the customer and you do not overwrite it. The email is proven by bZapper itself (new account) or by the code sent to the customer (existing account).
POST /partner/connect/exchange
Exchanges the code for the customer's API key. Single use.
{ "code": "cc_4291…" }
{ "id": "7ece…", "external_id": "customer-4821", "status": "active",
"account_id": "08ea…", "project_id": "88a2…",
"api_key": "bz_live_45cd…",
"numbers": [{ "id": "209b…", "phone": "+5511988887777", "status": "connected" }] }
The api_key is not shown again. Errors: 400 code_required, 400 invalid_code
(wrong, expired, already used, or from another partner), 409 connection_revoked.
GET /partner/connections
Lists your connections. Filters: external_id, status. Returns { "data": [...] }
with the connection object.
GET /partner/connections/{id}
One connection, with a fresh numbers[]. 404 connection_not_found if it is not yours.
POST /partner/connections/{id}/rotate-key
Mints a new API key and revokes the previous one immediately. Use it when you lost the key, or when the webhook told you about the completion but you never exchanged the code.
Errors: 404 connection_not_found, 409 connection_not_active (not completed yet, or
already ended).
DELETE /partner/connections/{id}
Ends the connection on your side: revokes the key and fires connect.revoked with
revoked_by: "partner". It does not cancel the customer's plan — that subscription
is theirs with bZapper. Returns 204.
4. Connection object
{
"id": "7ece7f98-…",
"external_id": "customer-4821",
"status": "active",
"account_id": "08eaa5be-…",
"project_id": "88a27b8c-…",
"customer": { "name": "Ana Souza", "email": "[email protected]",
"company": "Boxy Pharma", "phone": "+5511988887777", "country": "BR" },
"numbers": [{ "id": "209bd3cd-…", "phone": "+5511988887777", "status": "connected" }],
"activated_at": "2026-09-17T16:55:02Z",
"suspended_at": null,
"revoked_at": null,
"created_at": "2026-09-17T16:40:00Z"
}
In GET /me/connections (the customer, in their own dashboard) the same object carries
partner_name and partner_logo_url, and never an api_key.
5. State machine
pending_account ──account linked──▶ pending_payment ──Pro paid──▶ pending_number
│
number connected + complete
▼
revoked ◀──customer/partner/account deleted── active
▲ │ ▲
└──────────────────────────────────────────────┘ │
Pro unpaid ──▶ suspended
Pro paid ────┘
status | Meaning | The partner API key |
|---|---|---|
pending_account | Session open, account not linked yet | does not exist |
pending_payment | Account linked, Pro unpaid | does not exist |
pending_number | Pro paid, WhatsApp not connected | does not exist |
active | Completed | works |
suspended | The customer's Pro is unpaid | 402 connect_suspended |
revoked | Ended | 401 connect_revoked |
Who changes the state:
- You:
DELETE /partner/connections/{id}→revoked. - The customer: "Connected apps" in the dashboard →
revoked; paying or not paying the Pro →active/suspended. - bZapper: a sweeper runs every 2 minutes (and on a plan downgrade) and reconciles:
Pro unpaid →
suspended; Pro paid again →active; account or project deleted →revoked. The check also runs on every call made with the key, so suspension and revocation take effect immediately, without waiting for the sweeper.
revoked is terminal. The connection releases the account (it loses account_id /
project_id), the key dies, and every /connect/* route answers
409 connection_revoked — even with a ticket still inside its 30 minutes. Opening a new
session with the same external_id restarts at the account step, and since the email
already has a bZapper account, it requires the code sent to the customer. You cannot
reconnect on your own.
6. Partner webhooks
A single endpoint, registered with you, receives the events of all your connections.
Signature. X-Bzapper-Signature: sha256=<hex> = HMAC-SHA256 of the raw body with
the partner webhook secret (which is not the bz_partner_). Verify before parsing.
Extra headers: X-Bzapper-Event-Id, X-Bzapper-Event-Type.
import hmac, hashlib
def valid(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Envelope. The standard bZapper one plus the connection block:
{
"event_id": "evt_3f…",
"event_type": "message.received",
"timestamp": "2026-09-17T16:55:02Z",
"instance_id": "209bd3cd-…",
"client_reference": "ticket-991",
"payload": { "type": "text", "from": "+5511999990000", "body": "Hi!" },
"connection": {
"id": "7ece7f98-…", "external_id": "customer-4821",
"account_id": "08eaa5be-…", "project_id": "88a27b8c-…", "status": "active"
}
}
Lifecycle events
| Event | When | payload |
|---|---|---|
connect.completed | The customer finished (Pro paid + WhatsApp connected) | status |
connect.suspended | The Pro is no longer paid | status |
connect.resumed | Paid, and the connection is back | status |
connect.revoked | Ended | status, revoked_by: customer | partner | account_deleted | project_deleted |
Operational events
For active connections, the same bZapper project events: message.received,
message.sent, message.delivered, message.read, message.failed,
instance.connected, instance.warming, instance.disconnected,
instance.logged_out, instance.banned, contact.opted_out and group.*.
They cover any number in the connection's project — including one the customer adds
themselves in the bZapper dashboard: you get instance.connected and never need to poll.
Not forwarded: qr_code and pairing_code. They are the secret of pairing the
customer's device.
Delivery
- Up to 5 attempts with exponential backoff (1s, 2s, 4s, 8s).
- We retry on transport failures,
5xxand429. A4xx(wrong URL, rejected signature) is not retried — fix the endpoint. - At-least-once: deduplicate by
event_id. - No history replay: you receive what happens from the connection onwards. To reconcile
state, use
GET /partner/connections. - No webhook needs to be registered on the customer's account, and your channel does not compete with the "one webhook per event per project" rule that applies to theirs.
7. The embedded component
<script src="https://widget.bzapper.com.br/v1/connect.js"></script>
The bundle is self-contained (Preact + QR + i18n), mounts in Shadow DOM and uses no
iframe for its own screen. The card form is Stripe's own, mounted in a light-DOM node
projected through a <slot> — Stripe's iframes do not work inside Shadow DOM.
Modal
const modal = BzapperConnect.open({
session: 'cs_…', // required
apiBase: 'https://api.bzapper.com.br',
locale: 'en',
onReady: ({ step, connectionId, status }) => {},
onStep: ({ step }) => {},
onComplete: ({ code, connectionId, externalId }) => {},
onClose: () => {},
onError: ({ code, message }) => {},
});
modal.close(); // close from code
Inline
<bzapper-connect data-session="cs_…" data-api="https://api.bzapper.com.br" data-locale="en"></bzapper-connect>
Equivalent DOM events, with bubbles and composed: bzapper:ready, bzapper:step,
bzapper:complete, bzapper:close, bzapper:error. The detail is the same object as
the callbacks receive.
Steps (step)
step | Screen | Leaves when |
|---|---|---|
account | Asks "Do you already have a bZapper account?": Create new account (confirm the details) or I have an account (link by email) | account created, or code sent |
verify_email | 6-digit code (existing account) | code confirmed |
payment | Pro subscription (card or Pix) | payment confirmed |
number | QR code or pairing code | number connected |
manage | Connected numbers (connection completed) | — |
revoked | Connection ended | — |
Languages: pt, en, es, it, de, fr. Without locale, it uses the document's
lang and then the browser's.
CSP on your site
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;
Endpoints the component consumes
You do not need to call these — they are here for debugging. All require
X-Connect-Session and a registered origin.
| Route | Purpose |
|---|---|
GET /connect/bootstrap | Screen state: partner, customer, step, plan, numbers, Stripe publishable key |
POST /connect/account | Creates the account (or sends the code, if the email already has one) |
POST /connect/account/verify | Confirms the 6-digit code |
POST /connect/account/resend | Resends the code to the same email (once per minute) |
POST /connect/account/link | "I have an account": {email} (empty = the email the partner sent) → code to that email |
POST /connect/account/link/cancel | Leaves the code step (change email or create a new account) |
POST /connect/checkout | Subscribes to Pro with one method (card | pix) and returns the client_secret |
GET /connect/payment | Payment confirmation (polled by the component while it waits) |
GET /connect/numbers · POST /connect/numbers | Lists and creates the number |
POST /connect/numbers/{id}/connect?method=qr|code | Generates a QR or a pairing code |
POST /connect/numbers/{id}/disconnect | Disconnects |
GET /connect/stream | Project SSE: qr_code, pairing_code, instance.status |
POST /connect/complete | Completes and issues the code |
8. Limits of the delivered API key
The key is role: agent, carries the scopes from your registration, and is bound to
the connection's project.
Reaches: /instances, /messages, /chats, /labels, /calls, /groups,
/conversations, /presence, /pools, /campaigns, /usage, /advisories,
POST /contacts/check, plus the reads GET /me, GET /me/entitlements,
GET /me/subscription.
Does not reach (403 forbidden): /keys, /users, /projects, /account*,
/me/plan*, /me/addons*, /me/invoices*, /billing*, /webhooks*, /me/widgets,
/me/connections, /platform/*, /brand, /official.
Also out: GET /stream and the account address book (/contacts, /tags,
/contact-groups, /suppressions, /blocklist). Both are per account, not per
project — and the SSE feed still carries QR and pairing codes. You receive events through
your webhook, already filtered per connection.
Project fence: a number in another project of the same customer answers 404, and
GET /instances?project_id=all returns only the connection's project.
9. Billing
- The partner channel is Pro only. There is no Free tier: without a paid Pro there is no number to connect and no key to issue.
- Methods: card (turns on automatic renewal, the card is saved) and Pix (BRL only; renewals arrive as a payment link by email). Boleto is not part of Connect. If bZapper's Stripe account does not have Pix enabled, the component offers card only.
- The customer pays bZapper directly: invoice and receipt are in their name, and the card never goes through your code or ours.
- Switching method halfway (generated a Pix, came back to the card) cancels the previous
attempt. If that payment is being confirmed, the answer is
409 payment_pending— wait a few seconds and retry, instead of charging twice. - Payment lapse:
suspended+connect.suspended. Reopening the component with the sameexternal_idtakes the customer straight to the payment screen, with a notice. Once paid:active+connect.resumed, with no new authorization.
10. Existing account
The component always asks whether the customer already has a bZapper account. Those who do only connect:
- Detected on open. If the email you sent in the session already has an account,
GET /connect/bootstrapreturnsexisting: "admin"(can link) or"member"(belongs to an account without permission), and the component opens on I have an account with "We found your account" — the customer doesn't find out only after clicking "Create account". - Another email. The customer can link using an email other than the one you sent
(
POST /connect/account/link {email}). Email without an account →404 account_not_found. - We send a 6-digit code to the account's email (
step: "verify_email", with a maskedemail_hint). "Change email or create an account" discards the code (POST /connect/account/link/cancel). - That email must be an admin of the account (
403 account_admin_requiredfor a regular user). Without this rule, knowing someone's email would be enough to gain access to their account. - Once the code is confirmed, the connection uses the account's default project. If the account is already Pro, the payment step is skipped; if a number is already connected, the customer only authorizes.
Caps per target email, in a 1-hour window: 5 codes sent and 10 wrong attempts
(429 code_attempts_exceeded; the 6th send answers 429 code_sends_exceeded). The code expires in 10 minutes and dies on the 5th wrong
attempt of the same connection.
11. Error table
| Code | HTTP | Where | Meaning |
|---|---|---|---|
partner_unauthorized | 401 | /partner/* | Secret missing, wrong, rotated, or sent outside the header |
partner_inactive | 403 | all | Integration disabled by bZapper |
external_id_required | 400 | create session | Missing external_id (or over 200 characters) |
customer_email_required | 400 | create session | Email missing or invalid |
customer_name_required | 400 | create session | Missing name or company |
connect_session_required | 401 | component | No X-Connect-Session |
invalid_connect_session | 401 | component | Invalid ticket |
connect_session_expired | 401 | component | Ticket older than 30 min → open a new one |
origin_not_allowed | 403 | component | Page domain outside the registered origins |
connection_revoked | 409 | component | The customer ended it: open a new session (restarts at the account step) |
account_required | 409 | component | Step requested before the account was linked |
account_admin_required | 403 | component | The email has a bZapper account but is not an admin of it |
invalid_code | 400 | component | Wrong or expired email code |
code_attempts_exceeded | 429 | component | Per-target caps exceeded |
code_sends_exceeded | 429 | component | Too many codes sent to that email in the last hour |
account_not_found | 404 | component | "I have an account" with an email that has no bZapper account |
payment_required | 402 | component | Number or completion before the Pro is paid |
payment_pending | 409 | component | Previous attempt still being confirmed (Pix/3DS) |
already_paid | 409 | component | The plan is already paid |
method_unavailable | 422 | component | Method does not apply (e.g. Pix outside BRL) |
payment_unavailable | 502 | component | The method failed at Stripe (e.g. Pix not enabled) |
number_required | 409 | component | Completing without a connected number |
quota_exceeded | 402 | component | Plan limit on numbers |
invalid_code (exchange) | 400 | /partner/connect/exchange | Wrong, expired, used, or another partner's code |
connection_not_found | 404 | /partner/connections/* | Not yours |
connection_not_active | 409 | rotate-key | Connection not completed or already ended |
connect_suspended | 402 | API key | The customer's Pro is unpaid |
connect_revoked | 401 | API key | Connection ended |
forbidden | 403 | API key | Route outside a partner key's reach |
12. Operations (bZapper)
Partner registration lives under Platform → Partners in the dashboard and requires an admin of the platform tenant.
- Create: name, slug, logo (file upload: PNG, JPEG, WebP or SVG, up to 2 MB), allowed origins, webhook URL and key scopes. The partner secret and the webhook secret are shown once.
- Replace the logo:
POST /platform/partners/{id}/logo(multipart, fieldfile) — what the dashboard button does. The image goes to bZapper's CDN and the registration stores the URL. - Rotate:
New secretandNew webhook secretinvalidate the previous one immediately. - Deactivate: turning
Activeoff stops sessions from opening and makes every key of that partner answer401 connect_revoked. It is the emergency switch. - Connections: the list shows
external_id, customer, status and date.
On the customer side, Connected apps lists the partners linked to the account and allows disconnecting (account admins only).
13. Known limits
- Origins: exact match, no wildcard. Every new domain must be registered.
- Send idempotency:
client_referenceis an echo, not a unique key. A retry on your side duplicates the message — store the returned id and resend only what failed. - No replay of past events on the partner channel.
- Default project on an existing account: the connection uses the account's default project, not a dedicated one.
- Two partners on the same account and project receive the same events of that project.