Seções

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.

ts
const official = await wa.channels.create({
  provider: 'meta',
  kind: 'official',
  label: 'Marketing',
  credentials: {
    phoneNumberId: '...',
    wabaId: '...',
    accessToken: '...',
  },
})

Resposta 201

json
{
  "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.

ts
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

O 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:

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

Iniciar uma tentativa não-oficial exige um aceite de risco vigente registrado no broker (Compliance v1). Repasse-o inline em 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)

Há dois caminhos para o não-oficial. Normal (agnóstico): você não envia 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 por channels.connection(id), que devolve state, qr e phoneNumber.
  • channels.disconnect(id) — para de despachar pelo número, sem apagar o registro. Devolve o Channel.
  • channels.remove(id) — remove do pool (soft-delete: o histórico é preservado).

Resposta 200

json
{
  "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.

ts
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.

ts
const s = await wa.channels.status(official.id)

Resposta 200

json
{
  "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:

ts
// 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 com meta.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.
ts
// 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

json
{
  "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:

availabilitysignificado
connected_herejá é um canal deste workspace — channelId vem preenchido; conectar de novo é um no-op (mesmo channelId de volta).
availablelivre para conectar (canal vivo, sem dono ainda) — chame meta.connectNumber(phoneNumberId).
unavailablejá é canal de OUTRO workspace — conectar responde 409.
pending_registrationa Meta já conhece o número, mas ele ainda não virou canal — nada a conectar aqui.

channelId só aparece em connected_here

Por design, o 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

json
{
  "channelId": "ch_9f2c8a1b0d3e4f50"
}
statusquando
200número já era connected_here — no-op.
404channel_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).
409waba_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 cursor before.
  • channels.metrics({ window? }) — a saúde das conexões do tenant sobre uma janela (7d/30d/90d, default 30d): 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

Esses três bloqueios não disparam webhook (nem 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

json
{
  "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

json
{
  "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.