Receber webhooks
Configure um webhook no seu app e receba os eventos (campanha e canal) de todas as contas que autorizaram — com tenant_ref opaco, assinatura própria e sem tocar no webhook do cliente.
O OrbitSender pode empurrar eventos para o seu servidor via webhook — em vez de você ficar consultando (polling). Você configura um único webhook no seu app e ele recebe os eventos de todas as contas que autorizaram o app. São três tipos de evento, cada um com a sua própria URL: status de campanha, progresso de campanha e status de canal.
Um webhook no seu app — recebe de TODAS as contas
Este é o webhook do app (secundário do parceiro). Diferente do webhook que cada conta pode ter para uso próprio, o webhook do app é seu e agrega os eventos de todas as contas credenciadas:
- Você configura uma vez, no Portal do Desenvolvedor (no seu app → seção Webhook do app), não pela API externa e sem precisar de scope de webhook.
- Recebe de todas as contas que autorizaram o app. Cada evento traz um
tenant_refopaco identificando de qual conta ele é — o mesmo identificador que você recebe em/partner/me. Aponte quantas contas quiser para a mesma URL e diferencie pelotenant_ref. - Não substitui nem lê o webhook do cliente. O webhook próprio da conta (se houver) continua intacto — os dois coexistem. Você nunca sobrescreve a automação do cliente.
- Tem o próprio segredo de assinatura (
whsec_...), visível direto no Portal — você não depende de o cliente te passar segredo nenhum.
Quais eventos chegam
Um evento só chega de uma conta que autorizou o app e teve o scope relevante aprovado:
campaign_statusecampaign_progress→ contas que aprovaramcampaigns:read.channel_status→ contas que aprovaramchannels:read.
Se uma conta não te concedeu o scope, você não recebe aquele tipo de evento dela.
Como configurar
No Portal do Desenvolvedor, abra o seu app e vá em Webhook do app:
Informe as URLs
Preencha as URLs (HTTPS públicas) de campaign_status, campaign_progress e/ou channel_status. Deixe um campo em branco para não receber aquele tipo. Clique em Salvar webhook.
Guarde o segredo de assinatura
Ao salvar, o app ganha um segredo de assinatura (whsec_...). Copie-o — você vai usá-lo para validar a assinatura de cada requisição (seção abaixo). Dá para rotacioná-lo a qualquer momento (invalida o anterior).
Teste o seu endpoint
Use o botão Testar ao lado de cada URL. O OrbitSender dispara um POST de exemplo (com o header X-OrbitSender-Test: true e um tenant_ref de exemplo) para você validar recepção + assinatura antes de ir para produção.
Envelope comum
Todo evento chega como um POST com corpo JSON. O envelope sempre traz o campo event (o tipo), o tenant_ref (a conta que originou o evento — o mesmo id opaco do seu /partner/me) e um timestamp ISO 8601, além dos campos específicos do evento:
POST https://seuapp.com/webhooks/orbit
Content-Type: application/json
X-OrbitSender-Event: campaign_status1. campaign_status — status de campanha
Enviado para campaign_status_url a cada mudança de status de uma campanha.
{
"event": "campaign_status",
"tenant_ref": "361dd1gUOCmAuXuSSDpMNcSu",
"id_campaign": "campaign_2b1f...c9",
"name": "Promoção de sexta",
"status": "sending",
"timestamp": "2026-07-28T14:30:45.123Z"
}| Campo | Tipo | Descrição |
|---|---|---|
event | string | Sempre campaign_status. |
tenant_ref | string | Identificador opaco da conta que originou o evento — o mesmo do seu /partner/me. Use para saber de qual conta é o evento. |
id_campaign | string | Identificador externo da campanha (formato campaign_<uuid>) — o mesmo do GET /partner/campaigns. |
name | string | Nome da campanha. |
status | string | Ver a lista de valores abaixo. |
timestamp | string | ISO 8601 do momento do evento. |
Sem segmento e sem grupos crus
O evento não traz id_segment (token do segmento) nem JIDs de grupo — a superfície de parceiro não expõe esses identificadores. Os eventos carregam só o que o REST de parceiro já mostra: id_campaign, name, status e, no progresso, os contadores de grupos.
Valores de status: pending, scheduled, queued, sending, sent, failed, cancelled, error, interrupted, deleting, deleted, paused.
Vocabulário do webhook ≠ do polling REST
Esta lista é a do webhook. O polling REST GET /api/external/info-campaign/:id
usa um conjunto menor: pending, scheduled, queued, sending, sent,
paused, error, cancelled, deleting, deleted — sem failed nem
interrupted. Se você faz polling, trate falha como error (não failed).
2. campaign_progress — progresso de campanha
Enviado para campaign_progress_url conforme a campanha avança. Traz os mesmos campos de campaign_status mais os contadores de progresso:
{
"event": "campaign_progress",
"tenant_ref": "361dd1gUOCmAuXuSSDpMNcSu",
"id_campaign": "campaign_2b1f...c9",
"name": "Promoção de sexta",
"status": "sending",
"total_groups": 120,
"sent_groups": 84,
"error_groups": 2,
"timestamp": "2026-07-28T14:31:10.900Z"
}| Campo adicional | Tipo | Descrição |
|---|---|---|
total_groups | number | Total de grupos-alvo da campanha. |
sent_groups | number | Grupos já enviados. |
error_groups | number | Grupos que falharam. |
group_name | string? | Reservado. Campo opcional previsto no payload, mas o webhook de progresso agregado por campanha (o emissor atual) não o envia hoje. O JID cru do grupo nunca é enviado. |
3. channel_status — status de canal
Enviado para channel_status_url quando um canal de WhatsApp conecta ou desconecta.
{
"event": "channel_status",
"tenant_ref": "361dd1gUOCmAuXuSSDpMNcSu",
"channel_ref": "Q2hhbm5lbFJlZk9wYXF1ZQ",
"name": "Canal Principal",
"status": "CONNECTED",
"timestamp": "2026-07-28T14:32:00.000Z"
}| Campo | Tipo | Descrição |
|---|---|---|
event | string | Sempre channel_status. |
tenant_ref | string | Identificador opaco da conta (o mesmo do seu /partner/me). |
channel_ref | string | Referência opaca e estável do canal — o mesmo channel_ref que você recebe em list-channels (escopado ao seu app). Use para casar o evento com o canal do seu lado. O UUID cru e o telefone não são enviados. |
name | string | Nome do canal. |
status | string | CONNECTED ou DISCONNECTED. |
timestamp | string | ISO 8601. |
Headers
| Header | Valor |
|---|---|
Content-Type | application/json |
X-OrbitSender-Event | o tipo do evento (campaign_status | campaign_progress | channel_status) |
X-OrbitSender-Event-Id | UUID do evento — o mesmo entre retries (use para idempotência) |
X-OrbitSender-Signature | assinatura HMAC no formato t=<unix>,v1=<hmac_hex> (ver abaixo) |
X-OrbitSender-Timestamp | unix seconds usado na assinatura (anti-replay) |
X-OrbitSender-Test | true — apenas nos disparos do botão Testar do Portal |
Validar a assinatura
Cada webhook é assinado com HMAC-SHA256 usando o segredo de assinatura do seu app (whsec_...), que você vê e rotaciona no Portal do Desenvolvedor. Como o segredo é seu, você não depende de o cliente compartilhar nada.
O conteúdo assinado é "<timestamp>.<corpo_bruto>". Para validar:
import crypto from 'crypto';
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = parts.t, sig = parts.v1;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
// rejeite se a assinatura não bate ou se t está muito velho (ex.: > 5 min)
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Rotação do segredo
Você pode rotacionar o segredo no Portal (invalida o anterior). Ao rotacionar, os webhooks passam a ser assinados com o novo segredo imediatamente — atualize o valor no seu receptor.
Garantias de entrega (leia com atenção)
O modelo de entrega hoje é simples e best-effort. Projete o seu receptor sabendo que:
O que garantimos hoje
- Assinatura (HMAC): todo webhook vai assinado no header
X-OrbitSender-Signature— valide-o (seção acima) para confiar na origem. - Retry: até 3 tentativas com backoff (in-process, best-effort). Re-tentamos em erro de rede/timeout e em
5xx; um4xxdo seu endpoint para as tentativas. Como é in-process, um restart do servidor no meio ainda pode perder o evento — trate como best-effort. - Idempotência: o mesmo evento re-tentado carrega o mesmo
X-OrbitSender-Event-Id. De-duplique por ele. - Sem ordem garantida: eventos são despachados de forma assíncrona e independente; não assuma ordem entre eles.
- Timeout de 10 s: se o seu endpoint não responder em 10 segundos, a tentativa é abortada (e re-tentada).
Recomendações para o receptor:
- Valide a assinatura antes de confiar no payload.
- Responda
2xxrápido (em bem menos de 10 s); processe de forma assíncrona depois. Responder4xxcancela os retries. - Seja idempotente pelo
X-OrbitSender-Event-Id(ouid_campaign+status). - Use o
tenant_refpara rotear o evento para a conta certa no seu sistema (cruze com o/partner/me). - Reconcilie por polling quando precisar de garantia forte de entrega, já que os retries são best-effort.
Webhook do app ≠ webhook da conta
Existem dois webhooks distintos e independentes:
- Webhook do app (este): seu, configurado no Portal, recebe de todas as contas credenciadas com
tenant_refopaco e assinatura própria. - Webhook da conta: o do próprio dono da conta (configurado por ele em Configurações → Webhooks, ou pela api-key dele). Traz o
tenant_idcru e é assinado com o segredo da conta. Apps de parceiro não configuram, leem nem testam o webhook da conta — os scopeswebhooks:read/webhooks:writesão exclusivos do dono/api-key.