OrbitSenderDocs

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_ref opaco identificando de qual conta ele é — o mesmo identificador que você recebe em /partner/me. Aponte quantas contas quiser para a mesma URL e diferencie pelo tenant_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:

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_status

1. 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"
}
CampoTipoDescrição
eventstringSempre campaign_status.
tenant_refstringIdentificador opaco da conta que originou o evento — o mesmo do seu /partner/me. Use para saber de qual conta é o evento.
id_campaignstringIdentificador externo da campanha (formato campaign_<uuid>) — o mesmo do GET /partner/campaigns.
namestringNome da campanha.
statusstringVer a lista de valores abaixo.
timestampstringISO 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, deletedsem 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 adicionalTipoDescrição
total_groupsnumberTotal de grupos-alvo da campanha.
sent_groupsnumberGrupos já enviados.
error_groupsnumberGrupos que falharam.
group_namestring?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"
}
CampoTipoDescrição
eventstringSempre channel_status.
tenant_refstringIdentificador opaco da conta (o mesmo do seu /partner/me).
channel_refstringReferê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.
namestringNome do canal.
statusstringCONNECTED ou DISCONNECTED.
timestampstringISO 8601.

Headers

HeaderValor
Content-Typeapplication/json
X-OrbitSender-Evento tipo do evento (campaign_status | campaign_progress | channel_status)
X-OrbitSender-Event-IdUUID do evento — o mesmo entre retries (use para idempotência)
X-OrbitSender-Signatureassinatura HMAC no formato t=<unix>,v1=<hmac_hex> (ver abaixo)
X-OrbitSender-Timestampunix seconds usado na assinatura (anti-replay)
X-OrbitSender-Testtrueapenas 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; um 4xx do 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 2xx rápido (em bem menos de 10 s); processe de forma assíncrona depois. Responder 4xx cancela os retries.
  • Seja idempotente pelo X-OrbitSender-Event-Id (ou id_campaign + status).
  • Use o tenant_ref para 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_ref opaco 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_id cru e é assinado com o segredo da conta. Apps de parceiro não configuram, leem nem testam o webhook da conta — os scopes webhooks:read/webhooks:write são exclusivos do dono/api-key.

Nesta página