Webhooks

Um webhook avisa o seu sistema quando o status de uma mensagem muda, sem que você precise consultar a API.

Cada aviso é um POST JSON para a URL que você cadastrar, com até 100 eventos, ou com o que houver quando o evento mais antigo completar cerca de 5 segundos.

📘

Webhooks valem só para envios feitos pela API

Envios feitos pela interface do Notifica, e os de conta pessoal, não geram eventos. Para acompanhá-los, use a própria interface ou consulte GET /notifications/batch/{batch_id}.

Configurando

A configuração é feita no portal, na tela API > Webhook, e só admins da empresa têm acesso.

  • Cada empresa pode ter até 3 webhooks, cada um com nome, URL, eventos, canais, limite de requisições e segredo próprios. Assim dá para mandar, por exemplo, as falhas para um sistema de alertas e as entregas para o ERP.
  • A URL precisa ser HTTPS e apontar para um endereço público.
  • Escolha pelo menos um evento e um canal (email, sms, whatsapp) em cada webhook. Um evento só é enviado quando o tipo e o canal da mensagem estão assinados.
  • Um evento assinado por mais de um webhook é enviado a cada um deles, com um event_id diferente em cada. Entrega, pausa, limite e expiração valem por webhook: um endpoint fora do ar não atrasa os outros.
  • O botão Testar envia na hora um evento webhook.test para a URL, com o mesmo corpo, headers e assinatura de um evento real. Use-o para validar a URL e a verificação de assinatura antes de depender de um evento real.

Corpo da requisição

O corpo é sempre um envelope com uma lista de eventos, mesmo quando há um único evento:

{
  "delivery_id": "3f0c9a52-6d1e-4b7a-9c35-1a2b3c4d5e6f",
  "events": [
    {
      "event_id": "a7b8c9d0-1e2f-4a5b-8c6d-7e8f9a0b1c2d",
      "type": "message.failed",
      "occurred_at": "2026-05-28T19:40:51.874Z",
      "channel": "email",
      "status": "failed",
      "batch_id": "AB12CD34",
      "notification_id": 1234,
      "message_id": 5678,
      "contact_id": 910,
      "recipient": "[email protected]",
      "failure": {
        "reason": "bounced",
        "code": "5.1.1",
        "description": "550 5.1.1 <[email protected]>: Recipient address rejected: User unknown"
      }
    }
  ]
}
CampoTipoDescrição
delivery_idUUIDIdentifica a entrega. Se ela for reenviada, o delivery_id e os eventos são os mesmos.
eventslistaOs eventos da entrega.

Campos do evento

Todos os campos estão sempre presentes. O conteúdo do evento é fixado no momento em que ele acontece: uma retentativa não altera nada.

CampoTipoDescrição
event_idUUIDIdentificador único do evento. Use-o para deduplicar.
typestringTipo do evento (veja abaixo).
occurred_atstringHorário real do evento, ISO-8601 em UTC.
channelstringemail, sms ou whatsapp.
statusstringStatus da mensagem depois do evento: dispatched, delivered, failed, rejected, opened ou read.
batch_idstringO mesmo batch_id devolvido no 202 do envio.
notification_idinteiroServe no GET /notifications/{id}.
message_idinteiroIdentificador da mensagem.
contact_idinteiroIdentificador do contato.
recipientstring | nullE-mail ou telefone exatamente como enviado na requisição. null quando o contato não tem esse destino.
failureobjeto | nullPreenchido só em message.failed; null nos demais.

Tipos de evento

TipoQuando
message.sentA mensagem saiu do Notifica: e-mail aceito para envio pelo servidor de e-mail, ou SMS/WhatsApp entregue ao provedor (status: "dispatched"). Ainda não é a entrega ao destinatário.
message.deliveredE-mail aceito pelo servidor de destino, ou SMS/WhatsApp com entrega confirmada pelo provedor (status: "delivered").
message.failedA mensagem falhou (status: "failed") ou foi rejeitada pelo provedor de SMS/WhatsApp (status: "rejected"); o motivo vem em failure.
message.readE-mail aberto pela primeira vez (status: "opened") ou WhatsApp lido (status: "read"). Só a primeira abertura gera evento.
webhook.testEnviado só pelo botão Testar do portal, com dados fictícios (batch_id "TESTE-00" e ids 0). Responda 2xx e não o trate como aviso de mensagem.

Num SMS ou WhatsApp, occurred_at é o horário informado pelo provedor.

Falhas

Em message.failed, failure traz o motivo estruturado:

CampoTipoDescrição
reasonstringCategoria da falha, de uma lista fechada (abaixo).
codestring | nullCódigo técnico da falha: DSN (5.1.1) no e-mail, NTF-* nas falhas do Notifica. null quando não houver.
descriptionstring | nullTexto que explica a falha, quando houver.

Falhas de entrega

reasonCanalQuandocodedescription
bouncede-mailO servidor de destino recusou o e-mail.Código DSN (ex.: 5.1.1)Resposta SMTP do servidor
expirede-mailO e-mail ficou tempo demais sem ser entregue e foi descartado.Código DSN (ex.: 4.4.1)Último erro SMTP registrado
provider_failedqualquerO servidor de e-mail ou o provedor de SMS/WhatsApp falhou por outro motivo.DSN, quando houverTexto do erro, quando houver
provider_rejectedSMS, WhatsAppO provedor rejeitou a mensagem.nullTexto do erro, quando houver

Adiamentos temporários do servidor de destino não geram evento: o e-mail continua sendo tentado até ser entregue, recusado ou expirar.

Falhas do próprio Notifica

Quando a mensagem não chega a sair por causa do próprio Notifica, o reason é um dos abaixo, em qualquer canal. code é o NTF-* que identifica a causa e description vem sempre "internal".

reasonQuandocode
no_emailO contato não tem e-mail; recipient vem null.NTF-NOTIF-125
no_phoneO contato não tem telefone (SMS ou WhatsApp); recipient vem null.NTF-NOTIF-124
template_not_activeO template de WhatsApp não estava ativo no momento do envio.NTF-NOTIF-123
skipped_missing_variableO contato não tinha valor para uma variável usada na mensagem.NTF-NOTIF-126
attachment_missingUm anexo da mensagem não foi encontrado no momento do envio.NTF-NOTIF-127
document_missingO documento da mensagem não estava disponível no momento do envio.NTF-NOTIF-128
prepare_failedNão foi possível preparar a mensagem.NTF-NOTIF-131, ou o código da causa (ex.: NTF-TMPL-101)
dispatch_failedNão foi possível entregar a mensagem ao canal.NTF-NOTIF-132, ou o código da causa
not_attemptedO prazo do lote venceu antes de a mensagem sair.NTF-NOTIF-129
deadline_exceededA mensagem saiu, mas o canal não confirmou a entrega dentro do prazo do lote.NTF-NOTIF-130

🚧

Falha nem sempre é final

Em casos raros, uma mensagem já avisada com message.failed é entregue depois, e chega um message.delivered para o mesmo message_id. É ele que vale: considere a mensagem entregue.

Headers

HeaderValor
Content-Typeapplication/json
User-AgentBryNotifica-Webhook/1.0
X-Notifica-DeliveryO mesmo delivery_id do corpo.
X-Notifica-Signaturet=<unix>,v1=<hex>: HMAC-SHA256 de "{t}.{corpo}" com o segredo do webhook.

Verificando a assinatura

Calcule o HMAC-SHA256 de "{t}.{corpo}" com o segredo do webhook que recebeu a requisição (mostrado na tela API > Webhook) e compare com v1 em tempo constante. Use o corpo bruto, exatamente como recebido, antes de qualquer parse. Recuse requisições cujo t esteja longe do horário atual, para barrar replays.

[$t, $v1] = sscanf($_SERVER['HTTP_X_NOTIFICA_SIGNATURE'], 't=%d,v1=%s');
$body = file_get_contents('php://input');

$valid = hash_equals(hash_hmac('sha256', "{$t}.{$body}", $secret), $v1)
    && abs(time() - $t) <= 300;
const crypto = require('crypto');

const [, t, v1] = req.headers['x-notifica-signature'].match(/^t=(\d+),v1=([0-9a-f]+)$/);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');

const valid = expected.length === v1.length
  && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
  && Math.abs(Date.now() / 1000 - Number(t)) <= 300;

Resposta esperada

Responda qualquer 2xx para confirmar todos os eventos da requisição. Só o status conta: o corpo da resposta nem é lido. Responda rápido e processe os eventos depois, porque um envio grande gera muitas requisições em sequência. Redirecionamentos não são seguidos.

Todo o resto conta como falha: resposta fora de 2xx, sem resposta em 10 segundos, sem conexão em 5 segundos, erro de TLS (certificado inválido, expirado ou de autoridade não confiável), ou URL que deixou de ser HTTPS ou passou a apontar para um endereço interno. A entrega é reenviada depois com o mesmo delivery_id e os mesmos eventos.

Pausa e reenvio

Quando uma entrega falha, o Notifica pausa os envios para aquele webhook (os outros seguem normalmente) por um tempo que cresce a cada falha seguida:

Falha seguidaPausa
1ª1 minuto
2ª5 minutos
3ª30 minutos
4ª2 horas
5ª6 horas
6ª em diante12 horas

Durante a pausa os eventos continuam sendo registrados. Ao fim dela, a entrega mais antiga é reenviada e, na primeira resposta 2xx, a contagem recomeça e os eventos acumulados saem em requisições de até 100 eventos. Salvar a configuração ou um teste bem-sucedido encerra a pausa na hora.

Limite de requisições

Por padrão não há limite. Para proteger o seu endpoint, defina no webhook um limite de requisições por minuto (de 1 a 1000): o Notifica espaça as requisições em pelo menos 60 / limite segundos, inclusive os reenvios. Como cada requisição leva até 100 eventos, o limite define a vazão máxima: até 100 × limite eventos por minuto.

Expiração e desativação

Um evento sem resposta 2xx em 24 horas desde que foi registrado é descartado e não é mais enviado: escolha um limite de requisições que dê conta do seu volume. Se o seu endpoint ficar fora do ar por mais tempo, use GET /notifications/batch/{batch_id} para recuperar o que foi perdido.

Desativar o webhook descarta os eventos pendentes, e nenhum evento é registrado enquanto ele estiver desativado; reativar não recupera esse período. A URL, os eventos, os canais, o limite e o segredo continuam os mesmos.

Garantias

  • Pelo menos uma vez: o mesmo evento pode chegar mais de uma vez. Deduplique por event_id.
  • Sem ordem: eventos podem chegar fora de ordem. Ordene por occurred_at quando precisar.
  • Tipos desconhecidos: ignore tipos de evento que você não conhece; novos tipos podem ser adicionados.