Guia: Webhooks
Receba notificações em tempo real quando eventos fiscais acontecem. Configuração, segurança HMAC e boas práticas.
Webhooks
Webhooks são chamadas HTTP que a engineAPI faz para sua URL quando um evento acontece (nota autorizada, rejeitada, certificado vencendo). Você não precisa fazer polling.
Cada partner tem uma única configuração de webhook (uma URL + uma lista de eventos),
não múltiplos webhooks cadastráveis. Configure via PATCH /v1/webhooks/config.
Configurando um Webhook
curl -X PATCH https://api.engineapi.com.br/v1/webhooks/config \
-H "Authorization: Bearer SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://seusite.com/webhooks/engine",
"events": ["invoice.authorized", "invoice.rejected", "certificate.expiring"]
}'
await fetch('https://api.engineapi.com.br/v1/webhooks/config', {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
webhookUrl: 'https://seusite.com/webhooks/engine',
events: ['invoice.authorized', 'invoice.rejected', 'certificate.expiring'],
}),
});
Não há campo secret no PATCH /webhooks/config: o secret HMAC é gerado pela API
(ver Secret e Assinatura abaixo), nunca fornecido por você.
Consultar a configuração atual:
curl https://api.engineapi.com.br/v1/webhooks/config \
-H "Authorization: Bearer SEU_TOKEN"
Eventos disponíveis
Qualquer valor fora desta lista em events[] é rejeitado com 400:
| Evento | Quando dispara |
|---|---|
invoice.authorized | NFe ou NFCe autorizada pela SEFAZ |
invoice.rejected | NFe ou NFCe rejeitada pela SEFAZ |
invoice.canceled | NFe ou NFCe cancelada |
mdfe.authorized | MDFe autorizado |
mdfe.closed | MDFe encerrado |
cte.authorized | CTe autorizado |
cte.canceled | CTe cancelado |
cte.cce | Carta de Correção de CTe |
dfe.received | Novo documento recebido na distribuição (DFe) |
certificate.expiring | Certificado a 30/15/7/3/1 dia(s) de expirar |
sefaz.status.changed | Diferencial: mudança real de status de uma UF na SEFAZ (up→down ou down→up) |
invoice.correction e nfse.authorized não existem neste enum: CC-e de NFe não
dispara webhook próprio hoje, e NFSe autorizada entra em invoice.authorized (com
model: "NFSe" no data). Não assine eventos fora desta tabela.
Os eventos cte.* e mdfe.* existem no enum, mas CTe e MDFe estão fora da superfície
pública da engineAPI hoje (roadmap, sem previsão de data; ver Guia: CTe
e Guia: MDFe). Na prática, esses eventos são inalcançáveis: não há
endpoint público para emitir um CTe ou MDFe que os dispare.
dfe.received também existe no enum, mas o módulo DFe está fora do contrato público
hoje: não há endpoint público para configurar a distribuição de DFe que dispararia
este evento. Não assine até a funcionalidade entrar na superfície pública.
Estrutura do Payload
O campo é type, não event:
{
"id": "9f1c2b3a-4c5d-4e6f-8a9b-0c1d2e3f4a5b",
"type": "invoice.authorized",
"timestamp": "2026-04-26T23:00:00.000Z",
"data": {
"invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"model": "55",
"number": 1,
"series": 1,
"accessKey": "35260211222333000181550010000000011000000019",
"status": "AUTHORIZED",
"amount": "119.8"
}
}
amount é string decimal ("119.8"), nunca number cru: evita imprecisão de
ponto flutuante em valor monetário. Não trate como número sem converter explicitamente
no seu código. A resposta HTTP de POST /v1/nfe usa o mesmo formato.
id é um UUID cru: não tem prefixo evt_.
No invoice.rejected, o data traz status: "REJECTED" e o campo erros[]
com o retorno verbatim da SEFAZ/SEFIN (mesmo shape do 400 da emissão
síncrona); ramifique pelo codigo. Na NFSe, o identificador em data é
nfseId (não invoiceId) e model: "NFSe":
{
"id": "9f1c2b3a-...-uuid",
"type": "invoice.rejected",
"timestamp": "2026-07-05T11:06:14.453Z",
"data": {
"invoiceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"model": "55",
"number": 321,
"series": 1,
"accessKey": null,
"status": "REJECTED",
"amount": 10,
"erros": [
{
"codigo": "696",
"descricao": "Rejeicao: Obrigatoria as informacoes do responsavel tecnico pela emissao do DF-e"
}
]
}
}
O campo id do evento é único e pode ser usado para garantir idempotência. Se o mesmo evento chegar duas vezes, ignore a duplicata.
sefaz.status.changed: status real da SEFAZ por UF (diferencial)
A engineAPI monitora as 27 UFs com certificado digital próprio (não depende do seu certificado) a cada 5 minutos. Quando uma UF muda de estado de verdade (a SEFAZ respondeu que ficou indisponível, ou voltou a operar), o evento dispara pra quem assinou. Nunca dispara numa consulta isolada (seria spam); só na transição.
{
"id": "9f1c2b3a-...-uuid",
"type": "sefaz.status.changed",
"timestamp": "2026-07-13T14:05:00.000Z",
"data": {
"uf": "GO",
"status": "down",
"cStat": 108,
"xMotivo": "Serviço Paralisado Temporariamente",
"ambiente": "producao",
"checkedAt": "2026-07-13T14:05:00.000Z"
}
}
| Campo | Descrição |
|---|---|
uf | UF autorizadora que mudou de estado |
status | "up" ou "down", sempre um estado REAL confirmado pela SEFAZ, nunca fabricado |
cStat | Código de status cru da SEFAZ (verbatim, ex.: 107 operacional, 108/109 parado) |
xMotivo | Mensagem cru da SEFAZ (verbatim) |
ambiente | "producao" ou "homologacao", ambiente monitorado |
checkedAt | Timestamp ISO8601 da consulta que detectou a transição |
Falha nossa ao consultar (rede, timeout, certificado de monitoramento vencido)
nunca vira status: "down": isso seria inventar uma indisponibilidade da SEFAZ que
não aconteceu. Sem resposta real confirmada, o estado fica "sem dado" internamente e
nenhum evento é disparado a partir dele.
Opt-in: assine sefaz.status.changed em events[] (PATCH /v1/webhooks/config)
como qualquer outro evento, não chega automaticamente.
Verificando a Autenticidade (HMAC)
A engineAPI assina cada webhook com HMAC SHA-256 usando o secret gerado pela API
(prefixo whsec_). Sempre valide a assinatura antes de processar o evento.
Os headers enviados são:
| Header | Conteúdo |
|---|---|
X-Webhook-Signature | sha256=<hash> |
X-Webhook-Event | O type do evento (ex.: invoice.authorized) |
X-Webhook-Delivery | ID da tentativa de entrega |
Use exatamente o nome de header da tabela acima.
import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/engine', (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const secret = process.env.WEBHOOK_SECRET!; // whsec_...
// Calcular HMAC esperado
const expected = 'sha256=' + createHmac('sha256', secret)
.update(req.body)
.digest('hex');
// Comparação segura contra timing attacks
const valid = timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!valid) {
return res.status(401).json({ error: 'Assinatura inválida' });
}
const { id, type, data } = JSON.parse(req.body.toString());
// Processar evento
handleEvent(id, type, data);
res.status(200).json({ received: true });
});
Nunca ignore a validação HMAC em produção. Qualquer endpoint sem verificação pode ser chamado por agentes maliciosos, injetando eventos falsos no seu sistema.
Secret e Regeneração
O secret HMAC (whsec_...) é gerado pela engineAPI, não escolhido por você:
curl -X POST https://api.engineapi.com.br/v1/webhooks/secret/regenerate \
-H "Authorization: Bearer SEU_TOKEN"
Regenerar invalida o secret anterior imediatamente: atualize sua variável de ambiente
WEBHOOK_SECRET no mesmo momento em que regenerar.
Recebendo e Processando Eventos
app.post('/webhooks/engine', (req, res) => {
// ... validação HMAC acima ...
const { id, type, data } = JSON.parse(req.body.toString());
// Idempotência: ignore eventos já processados
if (await isAlreadyProcessed(id)) {
return res.status(200).json({ received: true });
}
switch (type) {
case 'invoice.authorized':
await db.invoices.update({
where: { id: data.invoiceId },
data: { status: 'AUTHORIZED', accessKey: data.accessKey },
});
await notifyCustomer(data.invoiceId);
break;
case 'invoice.rejected':
await db.invoices.update({
where: { id: data.invoiceId },
data: { status: 'REJECTED' },
});
await alertSupport(data);
break;
case 'certificate.expiring':
await sendEmail({
to: 'admin@empresa.com',
subject: `⚠️ Certificado digital vencendo em ${data.daysUntilExpiry} dia(s)`,
body: `Renove o certificado do CNPJ ${data.cnpj}`,
});
break;
}
await markAsProcessed(id);
res.status(200).json({ received: true });
});
Política de Retentativas
Se seu endpoint retornar qualquer status diferente de 2xx ou não responder, a engineAPI
tenta novamente com o cronograma real (WebhooksService.RETRY_DELAYS), 5 tentativas no total:
| Tentativa | Aguarda | Total desde o evento |
|---|---|---|
| 1ª | imediato | 0s |
| 2ª | 5 minutos | 5min |
| 3ª | 30 minutos | 35min |
| 4ª | 2 horas | ~2h35min |
| 5ª | 24 horas | ~26h35min |
Após a 5ª tentativa falhar, o evento é movido para a Dead Letter Queue.
Dead Letter Queue (DLQ)
# Listar itens na DLQ
curl https://api.engineapi.com.br/v1/webhooks/dlq \
-H "Authorization: Bearer SEU_TOKEN"
# Reenviar um item específico
curl -X POST https://api.engineapi.com.br/v1/webhooks/dlq/{deliveryId}/retry \
-H "Authorization: Bearer SEU_TOKEN"
# Reenviar todos
curl -X POST https://api.engineapi.com.br/v1/webhooks/dlq/retry-all \
-H "Authorization: Bearer SEU_TOKEN"
# Limpar a DLQ
curl -X DELETE https://api.engineapi.com.br/v1/webhooks/dlq \
-H "Authorization: Bearer SEU_TOKEN"
A DLQ não vive em /admin/dead-letter-queue: é escopada por partner em /v1/webhooks/dlq.
GET /v1/webhooks/dlq só aceita limit (1-100, default 50): não pagina por page.
A resposta é { total, items[] }, não um array puro. Ver
Paginação.
Histórico de Entregas
curl https://api.engineapi.com.br/v1/webhooks/logs?limit=50 \
-H "Authorization: Bearer SEU_TOKEN"
GET /v1/webhooks/logs também só aceita limit (1-100, default 50): a resposta aqui
é um array puro dos deliveries (shape diferente do /dlq acima). Ver
Paginação.
Testando o webhook configurado
curl -X POST https://api.engineapi.com.br/v1/webhooks/test \
-H "Authorization: Bearer SEU_TOKEN"
Debugging Local
Para testar webhooks em desenvolvimento, use o webhook.site ou o ngrok:
# Com ngrok
ngrok http 3000
# Use a URL pública do ngrok ao cadastrar o webhook
# Ex: https://abc123.ngrok.io/webhooks/engine