Números & conexão
Criar números oficiais e não-oficiais, conectar por QR e acompanhar o ciclo de vida.
Um número é um número de WhatsApp que o broker opera por você. O namespace channels cria, conecta, supervisiona e remove números. A kind (official / unofficial) vem em todo número e define o que é possível.
Criar um número oficial
channels.create(input) é só para o oficial: recebe provider, kind, label e credentials com os dados da plataforma oficial (write-only: o broker cifra e nunca as devolve). O broker verifica na plataforma oficial antes de persistir; só um número já validado nasce connected. O não-oficial não usa mais create — veja Conectar por QR.
const official = await wa.channels.create({
provider: 'meta',
kind: 'official',
label: 'Marketing',
credentials: {
phoneNumberId: '...',
wabaId: '...',
accessToken: '...',
},
})Resposta 201
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": "5511",
"provider": "meta",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Todo Channel expõe kind (official / unofficial) e, quando pareado, number — o número humano real conectado (E.164 sem +). É o que distingue dois números não-oficiais de rótulo igual e a chave estável para amarrar conversas. É null antes de o número parear (não-oficial) ou de o oficial ser sincronizado com a plataforma oficial; depois de sincronizado, o oficial também expõe o number (que ainda se distingue pelo phoneNumberId).
Conectar por QR (não-oficial)
Um número não-oficial só nasce ao parear. O SDK separa uma tentativa de conexão efêmera do canal: channels.startConnect(input) cria a tentativa (não um canal); você faz polling por channels.connectStatus(attemptId); ao connected, o broker promove a tentativa a um canal real e devolve o channelId. Enquanto a tentativa vive, ela não aparece na listagem — não há "Número via QR" órfão.
import { RISK_TERMS_VERSION_UNOFFICIAL } from '@wabroker/sdk'
// 1) inicia a tentativa (sem credentials — o broker resolve a instância do pool).
// Exige o aceite de risco vigente (Compliance v1) inline.
const attempt = await wa.channels.startConnect({
label: 'Atendimento',
riskAcceptance: { termsVersion: RISK_TERMS_VERSION_UNOFFICIAL },
})
// attempt: { attemptId: 'cxn_…', state: 'connecting', qr: null }
// 2) poll até um estado terminal (connected | failed | expired)
let status = await wa.channels.connectStatus(attempt.attemptId)
while (status.state === 'connecting' || status.state === 'scan_qr') {
if (status.state === 'scan_qr') renderQr(status.qr) // o broker gera e renova; você só exibe
await sleep(2000)
status = await wa.channels.connectStatus(attempt.attemptId)
}
// 3) pareou — o canal real já existe
if (status.state === 'connected') {
console.log('canal:', status.channelId, 'número real:', status.number)
} else {
// 'failed' (status.lastError) ou 'expired' (o TTL venceu sem parear)
}O qr é o payload cru, não uma imagem
qr é o conteúdo cru do QR (a string a rasterizar numa imagem no seu lado) — não é PNG em base64 nem data: URI. Use uma lib de QR para desenhá-lo. O broker renova o código a cada ~20s, então o polling basta.ConnectStatus é uma união discriminada por state — estreite por narrowing:
| state | shape | significado |
|---|---|---|
connecting | { attemptId, state } | autenticando/carregando, sem QR pendente |
scan_qr | { attemptId, state, qr } | há um QR a exibir/renovar |
connected | { state, channelId, number? } | pareou e o canal foi promovido — opere por channelId |
failed | { state, lastError? } | o connector recusou (número já em uso, provider rejeitou) |
expired | { state } | o TTL da tentativa venceu sem parear (terminal) |
Aceite de risco (número não-oficial)
riskAcceptance: { termsVersion } — use a constante RISK_TERMS_VERSION_UNOFFICIAL que o SDK exporta (hoje 'unofficial-v1'). Alternativamente, registre-o antes com compliance.acceptRisk({ track: 'unofficial', termsVersion }) . Sem aceite, startConnect responde risk_acceptance_required (HTTP 422). O console repassa o aceite pelo SDK — nunca bloqueia por conta própria.Número não-oficial: pool (normal) ou BYO (avançado)
credentials — o broker resolve uma instância do pool. É o recomendado. Avançado (BYO): traga a sua instância em credentials (baseUrl obrigatório, apiKey opcional). O BYO só é honrado se o baseUrl estiver na allowlist SSRF do deployment (WAHA_ALLOWED_BASE_URLS); fora dela startConnect falha na guarda SSRF antes de persistir, e no BYO o preferredEngine é ignorado.channels.cancelConnect(attemptId) aborta uma tentativa em andamento (para e apaga a sessão best-effort, descarta a tentativa). Uma tentativa já promovida virou canal com sessão viva — cancelá-la aqui a derrubaria, então o broker recusa com attempt_not_cancellable (HTTP 409); use channels.remove(channelId) nesse caso.
Reconectar e desconectar
Isto é para números já existentes (não o onboarding acima).
channels.reconnect(id)— reabre uma sessão não-oficial que caiu (número já pareado), gerando um QR novo, sem criar outro número. Idempotente para um número jáconnected(não derruba a conexão viva). Depois, faça polling porchannels.connection(id), que devolvestate,qrephoneNumber.channels.disconnect(id)— para de despachar pelo número, sem apagar o registro. Devolve oChannel.channels.remove(id)— remove do pool (soft-delete: o histórico é preservado).
Resposta 200
{
"id": "ch_9f2c8a1b0d3e4f50",
"qr": "<redacted>",
"state": "scan_qr"
}Só para números oficiais
Registrar o número
channels.register(id, { pin }) executa a cerimônia do PIN da verificação em duas etapas da plataforma oficial, habilitando o número a enviar/receber. O PIN vai só no corpo (server-side); o broker nunca o persiste nem ecoa. Idempotente. Um número não-oficial rejeita.
await wa.channels.register(official.id, { pin: '000000' })Ler o estado na plataforma oficial
channels.status(id) lê ao vivo, junto à plataforma oficial, o NumberStatus: nome verificado, verificação por código, aprovação do nome e qualidade da linha.
const s = await wa.channels.status(official.id)Resposta 200
{
"codeVerificationStatus": "VERIFIED",
"nameStatus": "APPROVED",
"platformType": "CLOUD_API",
"qualityRating": "GREEN",
"throughput": {
"level": "STANDARD"
},
"verifiedName": "Acme Suporte"
}Embedded Signup (oficial, recomendado)
Em vez de coletar credenciais à mão, use o fluxo do Facebook. Seu backend pede a config pública, o browser abre o popup do FB SDK, e o backend conclui com o code devolvido:
// 1) config pública (appId, configId) para abrir o popup do FB SDK no browser
const cfg = await wa.meta.embeddedSignupConfig()
// 2) o popup devolve code + wabaId + phoneNumberId; seu backend conclui:
const channel = await wa.meta.completeEmbeddedSignup({
code, // curta duração — só no corpo, server-side
wabaId,
phoneNumberId,
})Precisa listar WABAs/números que um token alcança antes de escolher? meta.discover({ token, businessId? }) devolve a árvore negócios → WABAs → números. O token viaja só no corpo, server-side, e o broker não o persiste na descoberta.
Um complete pode terminar sem o usuário escolher/criar um número na WABA (ex.: o popup só associou a WABA a um Business existente) — omita phoneNumberId e o broker entra no ramo parcial: nenhum canal é criado, a árvore local de BM/WABA é sincronizada (fire-and-forget) e a resposta é 202 { synced: true } em vez do Channel (201). Depois disso, use meta.wabas() (abaixo) para listar os números que a sincronização revelou e conectar o que o usuário quiser.
Conectar oficial pela WABA
Alternativa ao Embedded Signup para um número que já existe numa WABA que o workspace enxerga (por ter ao menos um canal seu nela) — sem repetir a cerimônia do popup. Há dois caminhos para chegar a um número oficial:
- Número já existente — liste as WABAs visíveis com
meta.wabas()e conecte o número escolhido commeta.connectNumber(phoneNumberId). - Ativo novo (número que ainda não existe na Meta) — use o Embedded Signup acima, que cria o número na plataforma oficial antes de conectar.
// 1) lista as WABAs (e números) visíveis a este workspace
const wabas = await wa.meta.wabas()
// 2) escolha um número 'available' dentro de uma dessas WABAs
const waba = wabas.find((w) => w.numbers.some((n) => n.availability === 'available'))
const number = waba?.numbers.find((n) => n.availability === 'available')
// 3) conecta — devolve o channelId do canal já promovido para este workspace
if (number) {
const { channelId } = await wa.meta.connectNumber(number.phoneNumberId)
}Resposta 200
{
"items": [
{
"businessName": "Acme Ltda",
"name": "WABA Suporte",
"numbers": [
{
"availability": "connected_here",
"channelId": "ch_9f2c8a1b0d3e4f50",
"displayNumber": "+55 11 40001111",
"phoneNumberId": "pn_1",
"verifiedName": "Loja 1"
},
{
"availability": "available",
"channelId": null,
"displayNumber": "+55 11 40002222",
"phoneNumberId": "pn_2",
"verifiedName": "Loja 2"
},
{
"availability": "unavailable",
"channelId": null,
"displayNumber": "+55 11 40003333",
"phoneNumberId": "pn_3",
"verifiedName": "Loja 3"
},
{
"availability": "pending_registration",
"channelId": null,
"displayNumber": "+55 11 40004444",
"phoneNumberId": "pn_4",
"verifiedName": "Loja 4"
}
],
"syncedAt": "2026-08-04T12:00:00.000Z",
"wabaId": "waba_1"
}
]
}Cada número numa WABA visível traz a sua availability, que resolve exatamente o que este workspace pode fazer com ele — nunca o snapshot cru gravado, sempre derivada do canal vivo:
| availability | significado |
|---|---|
connected_here | já é um canal deste workspace — channelId vem preenchido; conectar de novo é um no-op (mesmo channelId de volta). |
available | livre para conectar (canal vivo, sem dono ainda) — chame meta.connectNumber(phoneNumberId). |
unavailable | já é canal de OUTRO workspace — conectar responde 409. |
pending_registration | a Meta já conhece o número, mas ele ainda não virou canal — nada a conectar aqui. |
channelId só aparece em connected_here
channelId de um número vem null em qualquer availability diferente de connected_here — nunca vaza o id de um canal de outro workspace nem de um número que ainda não tem canal.meta.connectNumber(phoneNumberId) é seletivo: nada se auto-conecta. A conexão é sempre número a número, uma chamada explícita por número escolhido — mesmo dentro de uma WABA com vários números available.
Resposta 200
{
"channelId": "ch_9f2c8a1b0d3e4f50"
}| status | quando |
|---|---|
| 200 | número já era connected_here — no-op. |
| 404 | channel_not_found — phoneNumberId inexistente OU pertence a uma WABA que este workspace não enxerga (fail-closed: mesmo com um canal livre no pool, sem visibilidade da WABA a conexão não é alcançável). |
| 409 | waba_number_unavailable — o número já é canal de outro workspace. |
Auditoria e saúde
channels.events(id, { limit?, before? })— o timeline de auditoria do número (cada transição de estado e reatribuição de tenant), mais recentes primeiro, paginado por cursorbefore.channels.metrics({ window? })— a saúde das conexões do tenant sobre uma janela (7d/30d/90d, default30d): quedas, recuperações, uptime médio, tendência e detalhe por número.
O ciclo de vida do número também chega ao seu webhook: eventos channel.status (up/down) e channel.lifecycle (cada transição). Veja Webhooks.
Restrição, limitação e quarentena não têm webhook
channel.status nem channel.lifecycle) — e um número restrito continua com state: 'connected'. Descubra o bloqueio por polling de GET /v1/channels (campos restricted/limited/quarantine) ou por GET /v1/channels/:id/overview.Quando o número pareia ou volta a subir, o channel.status chega com state: 'connected' (e o phoneNumber):
Evento channel.status
{
"brokerTimestamp": "2026-08-04T12:00:00.000Z",
"channelId": "ch_9f2c8a1b0d3e4f50",
"channelKind": "unofficial",
"channelNumber": "5511999999999",
"data": {
"phoneNumber": "5511999999999",
"state": "connected"
},
"engine": "webjs",
"id": "evt_0a7d3f1c9b5e2648",
"provider": "waha",
"providerTimestamp": "2026-08-04T12:00:00.000Z",
"tenantId": "tn_1a5c9e3f7b2d4068",
"traceId": "tr_2f8b6d0a4c1e3597",
"type": "channel.status"
}Entregue por POST com o header X-WaBroker-Signature (HMAC) — valide-o antes de confiar no corpo. Veja Verificar a assinatura.
Quando a sessão cai, o mesmo evento chega com state: 'down' — o sinal para você alertar ou reconectar:
Evento channel.status.down
{
"brokerTimestamp": "2026-08-04T12:00:00.000Z",
"channelId": "ch_9f2c8a1b0d3e4f50",
"channelKind": "unofficial",
"channelNumber": "5511999999999",
"data": {
"state": "down"
},
"engine": "webjs",
"id": "evt_0a7d3f1c9b5e2648",
"provider": "waha",
"providerTimestamp": "2026-08-04T12:00:00.000Z",
"tenantId": "tn_1a5c9e3f7b2d4068",
"traceId": "tr_2f8b6d0a4c1e3597",
"type": "channel.status"
}Entregue por POST com o header X-WaBroker-Signature (HMAC) — valide-o antes de confiar no corpo. Veja Verificar a assinatura.