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_iddiferente 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.testpara 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"
}
}
]
}
| Campo | Tipo | Descrição |
|---|---|---|
delivery_id | UUID | Identifica a entrega. Se ela for reenviada, o delivery_id e os eventos são os mesmos. |
events | lista | Os 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.
| Campo | Tipo | Descrição |
|---|---|---|
event_id | UUID | Identificador único do evento. Use-o para deduplicar. |
type | string | Tipo do evento (veja abaixo). |
occurred_at | string | Horário real do evento, ISO-8601 em UTC. |
channel | string | email, sms ou whatsapp. |
status | string | Status da mensagem depois do evento: dispatched, delivered, failed, rejected, opened ou read. |
batch_id | string | O mesmo batch_id devolvido no 202 do envio. |
notification_id | inteiro | Serve no GET /notifications/{id}. |
message_id | inteiro | Identificador da mensagem. |
contact_id | inteiro | Identificador do contato. |
recipient | string | null | E-mail ou telefone exatamente como enviado na requisição. null quando o contato não tem esse destino. |
failure | objeto | null | Preenchido só em message.failed; null nos demais. |
Tipos de evento
| Tipo | Quando |
|---|---|
message.sent | A 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.delivered | E-mail aceito pelo servidor de destino, ou SMS/WhatsApp com entrega confirmada pelo provedor (status: "delivered"). |
message.failed | A mensagem falhou (status: "failed") ou foi rejeitada pelo provedor de SMS/WhatsApp (status: "rejected"); o motivo vem em failure. |
message.read | E-mail aberto pela primeira vez (status: "opened") ou WhatsApp lido (status: "read"). Só a primeira abertura gera evento. |
webhook.test | Enviado 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:
| Campo | Tipo | Descrição |
|---|---|---|
reason | string | Categoria da falha, de uma lista fechada (abaixo). |
code | string | null | Código técnico da falha: DSN (5.1.1) no e-mail, NTF-* nas falhas do Notifica. null quando não houver. |
description | string | null | Texto que explica a falha, quando houver. |
Falhas de entrega
reason | Canal | Quando | code | description |
|---|---|---|---|---|
bounced | O servidor de destino recusou o e-mail. | Código DSN (ex.: 5.1.1) | Resposta SMTP do servidor | |
expired | O e-mail ficou tempo demais sem ser entregue e foi descartado. | Código DSN (ex.: 4.4.1) | Último erro SMTP registrado | |
provider_failed | qualquer | O servidor de e-mail ou o provedor de SMS/WhatsApp falhou por outro motivo. | DSN, quando houver | Texto do erro, quando houver |
provider_rejected | SMS, WhatsApp | O provedor rejeitou a mensagem. | null | Texto 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".
reason | Quando | code |
|---|---|---|
no_email | O contato não tem e-mail; recipient vem null. | NTF-NOTIF-125 |
no_phone | O contato não tem telefone (SMS ou WhatsApp); recipient vem null. | NTF-NOTIF-124 |
template_not_active | O template de WhatsApp não estava ativo no momento do envio. | NTF-NOTIF-123 |
skipped_missing_variable | O contato não tinha valor para uma variável usada na mensagem. | NTF-NOTIF-126 |
attachment_missing | Um anexo da mensagem não foi encontrado no momento do envio. | NTF-NOTIF-127 |
document_missing | O documento da mensagem não estava disponível no momento do envio. | NTF-NOTIF-128 |
prepare_failed | Não foi possível preparar a mensagem. | NTF-NOTIF-131, ou o código da causa (ex.: NTF-TMPL-101) |
dispatch_failed | Não foi possível entregar a mensagem ao canal. | NTF-NOTIF-132, ou o código da causa |
not_attempted | O prazo do lote venceu antes de a mensagem sair. | NTF-NOTIF-129 |
deadline_exceeded | A 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 ummessage.deliveredpara o mesmomessage_id. É ele que vale: considere a mensagem entregue.
Headers
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | BryNotifica-Webhook/1.0 |
X-Notifica-Delivery | O mesmo delivery_id do corpo. |
X-Notifica-Signature | t=<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 seguida | Pausa |
|---|---|
| 1ª | 1 minuto |
| 2ª | 5 minutos |
| 3ª | 30 minutos |
| 4ª | 2 horas |
| 5ª | 6 horas |
| 6ª em diante | 12 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_atquando precisar. - Tipos desconhecidos: ignore tipos de evento que você não conhece; novos tipos podem ser adicionados.

