Skip to main content

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:

PartyCredentialLives inReach
Your backendbz_partner_… (partner secret)your server onlyopen sessions, exchange the code, manage your connections
The component in the customer's browsercs_… (session ticket, 30 min)the browser, on your pageoperate one connection: account, payment, number
Your backend, operating WhatsAppbz_live_… (customer API key)your server onlyWhatsApp 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​

SurfaceHow it authenticatesNotes
/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.

What the origin protects — and what it does not

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.

FieldTypeRequiredDescription
external_idstring (≤200)yesThe customer id in your system. Use a stable id, never rotated.
customer.namestringname or companyThe person's name.
customer.emailstringyesDecides the path (new account × existing account).
customer.companystring—Becomes the account and project name on bZapper.
customer.phonestring (E.164)—Pre-fills the WhatsApp step.
customer.countrystring (ISO-3166 alpha-2)—Sets the currency: BR→BRL, Americas→USD, others→EUR. Pix is BRL only.
customer.localestring (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.

Customer data is a suggestion, not an identity

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 ────┘
statusMeaningThe partner API key
pending_accountSession open, account not linked yetdoes not exist
pending_paymentAccount linked, Pro unpaiddoes not exist
pending_numberPro paid, WhatsApp not connecteddoes not exist
activeCompletedworks
suspendedThe customer's Pro is unpaid402 connect_suspended
revokedEnded401 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.
Revocation is final

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​

EventWhenpayload
connect.completedThe customer finished (Pro paid + WhatsApp connected)status
connect.suspendedThe Pro is no longer paidstatus
connect.resumedPaid, and the connection is backstatus
connect.revokedEndedstatus, 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, 5xx and 429. A 4xx (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.

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)​

stepScreenLeaves when
accountAsks "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_email6-digit code (existing account)code confirmed
paymentPro subscription (card or Pix)payment confirmed
numberQR code or pairing codenumber connected
manageConnected numbers (connection completed)—
revokedConnection 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.

RoutePurpose
GET /connect/bootstrapScreen state: partner, customer, step, plan, numbers, Stripe publishable key
POST /connect/accountCreates the account (or sends the code, if the email already has one)
POST /connect/account/verifyConfirms the 6-digit code
POST /connect/account/resendResends 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/cancelLeaves the code step (change email or create a new account)
POST /connect/checkoutSubscribes to Pro with one method (card | pix) and returns the client_secret
GET /connect/paymentPayment confirmation (polled by the component while it waits)
GET /connect/numbers · POST /connect/numbersLists and creates the number
POST /connect/numbers/{id}/connect?method=qr|codeGenerates a QR or a pairing code
POST /connect/numbers/{id}/disconnectDisconnects
GET /connect/streamProject SSE: qr_code, pairing_code, instance.status
POST /connect/completeCompletes 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 same external_id takes 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:

  1. Detected on open. If the email you sent in the session already has an account, GET /connect/bootstrap returns existing: "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".
  2. 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.
  3. We send a 6-digit code to the account's email (step: "verify_email", with a masked email_hint). "Change email or create an account" discards the code (POST /connect/account/link/cancel).
  4. That email must be an admin of the account (403 account_admin_required for a regular user). Without this rule, knowing someone's email would be enough to gain access to their account.
  5. 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​

CodeHTTPWhereMeaning
partner_unauthorized401/partner/*Secret missing, wrong, rotated, or sent outside the header
partner_inactive403allIntegration disabled by bZapper
external_id_required400create sessionMissing external_id (or over 200 characters)
customer_email_required400create sessionEmail missing or invalid
customer_name_required400create sessionMissing name or company
connect_session_required401componentNo X-Connect-Session
invalid_connect_session401componentInvalid ticket
connect_session_expired401componentTicket older than 30 min → open a new one
origin_not_allowed403componentPage domain outside the registered origins
connection_revoked409componentThe customer ended it: open a new session (restarts at the account step)
account_required409componentStep requested before the account was linked
account_admin_required403componentThe email has a bZapper account but is not an admin of it
invalid_code400componentWrong or expired email code
code_attempts_exceeded429componentPer-target caps exceeded
code_sends_exceeded429componentToo many codes sent to that email in the last hour
account_not_found404component"I have an account" with an email that has no bZapper account
payment_required402componentNumber or completion before the Pro is paid
payment_pending409componentPrevious attempt still being confirmed (Pix/3DS)
already_paid409componentThe plan is already paid
method_unavailable422componentMethod does not apply (e.g. Pix outside BRL)
payment_unavailable502componentThe method failed at Stripe (e.g. Pix not enabled)
number_required409componentCompleting without a connected number
quota_exceeded402componentPlan limit on numbers
invalid_code (exchange)400/partner/connect/exchangeWrong, expired, used, or another partner's code
connection_not_found404/partner/connections/*Not yours
connection_not_active409rotate-keyConnection not completed or already ended
connect_suspended402API keyThe customer's Pro is unpaid
connect_revoked401API keyConnection ended
forbidden403API keyRoute 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, field file) — what the dashboard button does. The image goes to bZapper's CDN and the registration stores the URL.
  • Rotate: New secret and New webhook secret invalidate the previous one immediately.
  • Deactivate: turning Active off stops sessions from opening and makes every key of that partner answer 401 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_reference is 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.