Referência — bZapper Connect
Esta página é a referência técnica completa do Connect. Para o passo a passo de implantação, comece pelo guia do bZapper Connect.
Base da API: https://api.bzapper.com.br · Componente: https://widget.bzapper.com.br/v1/connect.js
1. Modelo mental
O Connect liga três partes, cada uma com a sua credencial:
| Parte | Credencial | Onde vive | Alcance |
|---|---|---|---|
| Seu backend | bz_partner_… (secret do parceiro) | só no seu servidor | abrir sessões, trocar o code, gerir suas conexões |
| O componente no navegador do cliente | cs_… (ingresso da sessão, 30 min) | navegador, na sua página | operar uma conexão: conta, pagamento, número |
| Seu backend, operando o WhatsApp | bz_live_… (API key do cliente) | só no seu servidor | o WhatsApp do projeto da conexão |
A entidade durável é a conexão: um cliente seu (external_id) ligado a uma conta
bZapper. Ela é única por (parceiro, external_id) — o mesmo external_id sempre cai na
mesma conexão, o que torna POST /partner/connect-sessions idempotente.
O code (cc_…) existe só para atravessar o navegador sem carregar a API key: ele é de
uso único, vale 10 minutos e só é trocável pelo parceiro dono da conexão.
2. Autenticação
| Superfície | Como autentica | Observações |
|---|---|---|
/partner/* | Authorization: Bearer bz_partner_… | Só header. O secret não é aceito em query string. |
/connect/* | X-Connect-Session: cs_… | Em GET /connect/stream (SSE) também vale ?session=cs_…, porque o EventSource não manda header. Nas demais rotas a query é recusada. |
API do cliente (/messages, /instances…) | Authorization: Bearer bz_live_… | A key entregue pela troca do code. |
Além da credencial, /connect/* confere o header Origin contra as origens
cadastradas do parceiro (comparação exata, sem curinga; maiúsculas e barra final são
normalizadas). Origem fora da lista: 403 origin_not_allowed.
A allowlist de origens protege o cliente contra outra página tentar abrir o
componente. Ela não é um limite contra você, parceiro: Origin é um header comum,
que qualquer chamada de servidor pode escrever. O que autentica /connect/* é o
ingresso cs_…; trate-o como segredo e não o registre em log.
3. Endpoints do parceiro (/partner/*)
GET /partner/me
Confere o secret e devolve o seu cadastro.
{ "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
Abre (ou reaproveita) a conexão do seu cliente e emite o ingresso do componente.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
external_id | string (≤200) | sim | O id do cliente no seu sistema. Use um id estável, nunca rotacionado. |
customer.name | string | name ou company | Nome da pessoa. |
customer.email | string | sim | Define o caminho do fluxo (conta nova × conta existente). |
customer.company | string | — | Vira o nome da conta e do projeto no bZapper. |
customer.phone | string (E.164) | — | Preenche o passo do WhatsApp. |
customer.country | string (ISO-3166 alpha-2) | — | Define a moeda: BR→BRL, Américas→USD, demais→EUR. Pix só em BRL. |
customer.locale | string (BCP-47) | — | Idioma do componente e dos e-mails. |
{ "session_token": "cs_8a82…", "expires_at": "2026-09-17T17:20:00Z",
"connection": { "id": "7ece…", "external_id": "cliente-4821", "status": "pending_account" } }
Erros: 400 external_id_required, 400 customer_email_required, 400 customer_name_required,
401 partner_unauthorized, 403 partner_inactive.
Enquanto a conta não está vinculada, cada nova sessão atualiza o prefill. Depois de vinculada, os dados são do cliente e você não os reescreve. Quem prova o e-mail é o próprio bZapper (conta nova) ou o código enviado a ele (conta existente).
POST /partner/connect/exchange
Troca o code pela API key do cliente. Uso único.
{ "code": "cc_4291…" }
{ "id": "7ece…", "external_id": "cliente-4821", "status": "active",
"account_id": "08ea…", "project_id": "88a2…",
"api_key": "bz_live_45cd…",
"numbers": [{ "id": "209b…", "phone": "+5511988887777", "status": "connected" }] }
A api_key não é mostrada de novo. Erros: 400 code_required, 400 invalid_code
(errado, vencido, já usado, ou de outro parceiro), 409 connection_revoked.
GET /partner/connections
Lista as suas conexões. Filtros: external_id, status. Devolve { "data": [...] }
com o objeto de conexão.
GET /partner/connections/{id}
Uma conexão, com numbers[] atualizado. 404 connection_not_found se não for sua.
POST /partner/connections/{id}/rotate-key
Cunha uma nova API key e revoga a anterior na hora. Use quando perdeu a key ou quando soube da conclusão pelo webhook sem ter trocado o code.
Erros: 404 connection_not_found, 409 connection_not_active (ainda não concluída ou
já encerrada).
DELETE /partner/connections/{id}
Encerra a conexão do seu lado: revoga a key e dispara connect.revoked com
revoked_by: "partner". Não cancela o plano do cliente — a assinatura é dele com o
bZapper. Responde 204.
4. Objeto de conexão
{
"id": "7ece7f98-…",
"external_id": "cliente-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"
}
Em GET /me/connections (o cliente, no painel dele) o mesmo objeto vem com
partner_name e partner_logo_url, e sem api_key.
5. Máquina de estados
pending_account ──conta vinculada──▶ pending_payment ──Pro pago──▶ pending_number
│
número conectado + concluir
▼
revoked ◀──cliente/parceiro/conta excluída── active
▲ │ ▲
└──────────────────────────────────────────────┘ │
Pro não pago ──▶ suspended
Pro pago ──────┘
status | O que significa | A API key do parceiro |
|---|---|---|
pending_account | Sessão aberta, conta ainda não vinculada | não existe |
pending_payment | Conta vinculada, Pro não pago | não existe |
pending_number | Pro pago, WhatsApp não conectado | não existe |
active | Concluída | funciona |
suspended | O Pro do cliente não está pago | 402 connect_suspended |
revoked | Encerrada | 401 connect_revoked |
Quem muda o estado:
- Você:
DELETE /partner/connections/{id}→revoked. - O cliente: "Apps conectados" no painel →
revoked; pagar/deixar de pagar o Pro →active/suspended. - O bZapper: um varredor roda a cada 2 minutos (e no momento do rebaixamento de
plano) e reconcilia: sem Pro pago →
suspended; Pro pago de novo →active; conta ou projeto apagados →revoked. A checagem também acontece em toda chamada com a key, então suspensão e revogação valem na hora, sem esperar o varredor.
revoked é terminal. A conexão solta a conta (perde account_id/project_id), a
key morre e qualquer rota de /connect/* passa a responder 409 connection_revoked,
inclusive com um ingresso ainda dentro dos 30 minutos. Abrir uma sessão nova com o mesmo
external_id recomeça no passo da conta, e como o e-mail já tem conta bZapper, exige
o código enviado ao cliente. Você não reconecta sozinho.
6. Webhooks do parceiro
Um endpoint só, cadastrado com você, recebe os eventos de todas as suas conexões.
Assinatura. X-Bzapper-Signature: sha256=<hex> = HMAC-SHA256 do corpo cru com o
secret do webhook do parceiro (que não é o bz_partner_). Valide antes de dar parse.
Cabeçalhos extras: X-Bzapper-Event-Id, X-Bzapper-Event-Type.
import hmac, hashlib
def valido(corpo: bytes, assinatura: str, secret: str) -> bool:
esperado = "sha256=" + hmac.new(secret.encode(), corpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, assinatura)
Envelope. O padrão do bZapper mais o bloco connection:
{
"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": "Olá!" },
"connection": {
"id": "7ece7f98-…", "external_id": "cliente-4821",
"account_id": "08eaa5be-…", "project_id": "88a27b8c-…", "status": "active"
}
}
Eventos de ciclo de vida
| Evento | Quando | payload |
|---|---|---|
connect.completed | O cliente concluiu (Pro pago + WhatsApp conectado) | status |
connect.suspended | O Pro deixou de estar pago | status |
connect.resumed | Pagou e a conexão voltou | status |
connect.revoked | Encerrada | status, revoked_by: customer | partner | account_deleted | project_deleted |
Eventos de operação
Das conexões active, os mesmos eventos de projeto do bZapper: message.received,
message.sent, message.delivered, message.read, message.failed,
instance.connected, instance.warming, instance.disconnected,
instance.logged_out, instance.banned, contact.opted_out e group.*.
Vale para qualquer número do projeto da conexão — inclusive um que o cliente
adicione sozinho pelo painel do bZapper: você recebe instance.connected e não precisa
ficar reconciliando.
Não são repassados: qr_code e pairing_code. São o segredo do pareamento do
aparelho do cliente.
Entrega
- Até 5 tentativas com backoff exponencial (1s, 2s, 4s, 8s).
- Reenviamos em falha de transporte,
5xxe429. Um4xx(URL errada, assinatura recusada) não é retentado — corrija o endpoint. - Entrega pelo menos uma vez: deduplique por
event_id. - Não há replay de histórico: você recebe o que acontece a partir da conexão. Para
reconciliar estado, use
GET /partner/connections. - Não é preciso cadastrar webhook na conta do cliente, e o seu canal não disputa com a regra de "um webhook por evento por projeto" que vale para os webhooks dele.
7. Componente embutido
<script src="https://widget.bzapper.com.br/v1/connect.js"></script>
O bundle é autocontido (Preact + QR + i18n), monta em Shadow DOM e não usa iframe da
própria tela. O formulário de cartão é do Stripe, montado num nó no light DOM projetado
por <slot> — os iframes do Stripe não funcionam dentro de Shadow DOM.
Modal
const modal = BzapperConnect.open({
session: 'cs_…', // obrigatório
apiBase: 'https://api.bzapper.com.br',
locale: 'pt-BR',
onReady: ({ step, connectionId, status }) => {},
onStep: ({ step }) => {},
onComplete: ({ code, connectionId, externalId }) => {},
onClose: () => {},
onError: ({ code, message }) => {},
});
modal.close(); // fecha por código
Inline
<bzapper-connect data-session="cs_…" data-api="https://api.bzapper.com.br" data-locale="pt-BR"></bzapper-connect>
Eventos DOM equivalentes, com bubbles e composed: bzapper:ready, bzapper:step,
bzapper:complete, bzapper:close, bzapper:error. O detail é o mesmo objeto dos
callbacks.
Passos (step)
step | Tela | Sai quando |
|---|---|---|
account | Pergunta "Você já tem conta no bZapper?": Criar conta nova (confere os dados) ou Já tenho conta (vincula por e-mail) | conta criada, ou código enviado |
verify_email | Código de 6 dígitos (conta já existente) | código confirmado |
payment | Assinatura do Pro (cartão ou Pix) | pagamento confirmado |
number | QR code ou código de pareamento | número conectado |
manage | Números conectados (conexão concluída) | — |
revoked | Conexão encerrada | — |
Idiomas: pt, en, es, it, de, fr. Sem locale, usa o lang do documento e
depois o do navegador.
CSP do seu 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 que o componente consome
Você não precisa chamá-los — estão aqui para depuração. Todos exigem
X-Connect-Session e a origem cadastrada.
| Rota | Para quê |
|---|---|
GET /connect/bootstrap | Estado da tela: parceiro, cliente, passo, plano, números, chave publicável do Stripe |
POST /connect/account | Cria a conta (ou dispara o código, se o e-mail já tiver conta) |
POST /connect/account/verify | Confirma o código de 6 dígitos |
POST /connect/account/resend | Reenvia o código para o mesmo e-mail (1 por minuto) |
POST /connect/account/link | "Já tenho conta": {email} (vazio = o e-mail enviado pelo parceiro) → código para esse e-mail |
POST /connect/account/link/cancel | Sai do passo do código (trocar de e-mail ou criar conta nova) |
POST /connect/checkout | Assina o Pro num meio (card | pix) e devolve o client_secret |
GET /connect/payment | Confirmação do pagamento (o componente consulta enquanto espera) |
GET /connect/numbers · POST /connect/numbers | Lista e cria o número |
POST /connect/numbers/{id}/connect?method=qr|code | Gera QR ou código de pareamento |
POST /connect/numbers/{id}/disconnect | Desconecta |
GET /connect/stream | SSE do projeto: qr_code, pairing_code, instance.status |
POST /connect/complete | Conclui e emite o code |
8. Limites da API key entregue
A key é role: agent, com os escopos do seu cadastro, presa ao projeto da conexão.
Alcança: /instances, /messages, /chats, /labels, /calls, /groups,
/conversations, /presence, /pools, /campaigns, /usage, /advisories,
POST /contacts/check, e as leituras GET /me, GET /me/entitlements,
GET /me/subscription.
Não alcança (403 forbidden): /keys, /users, /projects, /account*,
/me/plan*, /me/addons*, /me/invoices*, /billing*, /webhooks*, /me/widgets,
/me/connections, /platform/*, /brand, /official.
Também fora: GET /stream e a base de contatos da conta (/contacts, /tags,
/contact-groups, /suppressions, /blocklist). Ambos são por conta, não por
projeto — e o SSE ainda carrega QR e código de pareamento. Você recebe os eventos pelo
seu webhook, já filtrado por conexão.
Cerca de projeto: um número de outro projeto do mesmo cliente responde 404, e
GET /instances?project_id=all devolve só o projeto da conexão.
9. Cobrança
- O canal de parceiros é só Pro. Não há Free: sem Pro pago não se conecta número nem se emite key.
- Meios: cartão (liga a renovação automática, o cartão fica salvo) e Pix (só em BRL; as renovações chegam por link no e-mail). Boleto não entra no Connect. Se a conta Stripe do bZapper não tiver o Pix ativo, o componente passa a oferecer só cartão.
- Quem paga é o cliente, direto ao bZapper: fatura e recibo saem no nome dele, e o cartão nunca passa pelo seu código nem pelo nosso.
- Trocar de meio no meio do caminho (gerou Pix, voltou para o cartão) cancela a tentativa
anterior. Se o pagamento anterior estiver em confirmação, a resposta é
409 payment_pending— espere alguns segundos e repita, em vez de cobrar duas vezes. - Queda de pagamento:
suspended+connect.suspended. Reabrir o componente com o mesmoexternal_idleva o cliente direto à tela de pagamento, com o aviso. Pagou:active+connect.resumed, sem nova autorização.
10. Conta existente
O componente sempre pergunta se o cliente já tem conta no bZapper. Quem já tem só conecta:
- Detecção na abertura. Se o e-mail que você mandou na sessão já tem conta, o
GET /connect/bootstraptrazexisting: "admin"(pode vincular) ou"member"(está numa conta, mas sem permissão), e o componente abre em Já tenho conta com "Encontramos sua conta" — o cliente não precisa descobrir isso clicando em "Criar conta". - Outro e-mail. O cliente pode vincular por um e-mail diferente do que você mandou
(
POST /connect/account/link {email}). E-mail sem conta →404 account_not_found. - Enviamos um código de 6 dígitos ao e-mail da conta (
step: "verify_email", comemail_hintmascarado). "Trocar e-mail ou criar conta" descarta o código (POST /connect/account/link/cancel). - O e-mail precisa ser administrador daquela conta (
403 account_admin_requiredpara um usuário comum). Sem isso, saber o e-mail de alguém bastaria para ganhar acesso à conta dela. - Confirmado o código, a conexão usa o projeto padrão da conta. Se a conta já for Pro, o passo de pagamento é pulado; se já houver número conectado, o cliente só autoriza.
Tetos por e-mail alvo, na janela de 1 hora: 5 códigos enviados e 10 tentativas
erradas (429 code_attempts_exceeded; o 6º envio responde 429 code_sends_exceeded). O código expira em 10 minutos e morre na 5ª
tentativa errada da mesma conexão.
11. Tabela de erros
| Código | HTTP | Onde | Significado |
|---|---|---|---|
partner_unauthorized | 401 | /partner/* | Secret ausente, errado, rotacionado ou enviado fora do header |
partner_inactive | 403 | todas | Integração desativada pelo bZapper |
external_id_required | 400 | criar sessão | Falta external_id (ou passa de 200 caracteres) |
customer_email_required | 400 | criar sessão | E-mail ausente ou inválido |
customer_name_required | 400 | criar sessão | Falta name ou company |
connect_session_required | 401 | componente | Sem X-Connect-Session |
invalid_connect_session | 401 | componente | Ingresso inválido |
connect_session_expired | 401 | componente | Ingresso passou dos 30 min → abra outro |
origin_not_allowed | 403 | componente | Domínio da página fora das origens cadastradas |
connection_revoked | 409 | componente | O cliente encerrou: abra uma sessão nova (recomeça no passo da conta) |
account_required | 409 | componente | Passo pedido antes de vincular a conta |
account_admin_required | 403 | componente | O e-mail tem conta bZapper, mas não é admin dela |
invalid_code | 400 | componente | Código de e-mail errado ou expirado |
code_attempts_exceeded | 429 | componente | Tetos por e-mail alvo estourados |
code_sends_exceeded | 429 | componente | Códigos enviados demais para aquele e-mail na última hora |
account_not_found | 404 | componente | "Já tenho conta" com um e-mail que não tem conta no bZapper |
payment_required | 402 | componente | Número ou conclusão antes do Pro pago |
payment_pending | 409 | componente | Tentativa anterior em confirmação (Pix/3DS) |
already_paid | 409 | componente | O plano já está pago |
method_unavailable | 422 | componente | Meio não serve (ex.: Pix fora do BRL) |
payment_unavailable | 502 | componente | O meio falhou no Stripe (ex.: Pix não ativo) |
number_required | 409 | componente | Concluir sem número conectado |
quota_exceeded | 402 | componente | Limite de números do plano |
invalid_code (troca) | 400 | /partner/connect/exchange | Code errado, vencido, já usado ou de outro parceiro |
connection_not_found | 404 | /partner/connections/* | Não é sua |
connection_not_active | 409 | rotate-key | Conexão não concluída ou encerrada |
connect_suspended | 402 | API key | Pro do cliente não pago |
connect_revoked | 401 | API key | Conexão encerrada |
forbidden | 403 | API key | Rota fora do alcance da key de parceiro |
12. Operação (bZapper)
O cadastro de parceiros fica em Plataforma → Parceiros, no painel, e exige administrador do tenant da plataforma.
- Criar: nome, slug, logo (upload do arquivo: PNG, JPEG, WebP ou SVG, até 2 MB), origens permitidas, URL do webhook e escopos da key. O secret do parceiro e o secret do webhook aparecem uma única vez.
- Trocar a logo:
POST /platform/partners/{id}/logo(multipart, campofile) — é o que o botão do painel faz. A imagem vai para a CDN do bZapper e o cadastro guarda a URL. - Rotacionar:
Novo secreteNovo secret do webhookinvalidam o anterior na hora. - Desativar: desliga
Ativo→ as sessões param de abrir e todas as keys daquele parceiro passam a responder401 connect_revoked. É o botão de emergência. - Conexões: a lista mostra
external_id, cliente, status e data de cada conexão.
Do lado do cliente, Apps conectados mostra os parceiros ligados à conta e permite desconectar (só administradores da conta).
13. Limites conhecidos
- Origens: comparação exata, sem curinga. Cada domínio novo precisa ser cadastrado.
- Idempotência no envio:
client_referenceé eco, não chave única. Um retry seu duplica a mensagem — guarde o id devolvido e reenvie só o que falhou. - Sem replay de eventos antigos no canal do parceiro.
- Projeto padrão em conta existente: a conexão usa o projeto padrão da conta, não um projeto dedicado.
- Dois parceiros na mesma conta e projeto recebem os mesmos eventos daquele projeto.