engineAPIengineAPI
// guias

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

bash
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"]
  }'
typescript
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:

bash
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:

EventoQuando dispara
invoice.authorizedNFe ou NFCe autorizada pela SEFAZ
invoice.rejectedNFe ou NFCe rejeitada pela SEFAZ
invoice.canceledNFe ou NFCe cancelada
mdfe.authorizedMDFe autorizado
mdfe.closedMDFe encerrado
cte.authorizedCTe autorizado
cte.canceledCTe cancelado
cte.cceCarta de Correção de CTe
dfe.receivedNovo documento recebido na distribuição (DFe)
certificate.expiringCertificado a 30/15/7/3/1 dia(s) de expirar
sefaz.status.changedDiferencial: 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:

json
{
  "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":

json
{
  "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.

json
{
  "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"
  }
}
CampoDescrição
ufUF autorizadora que mudou de estado
status"up" ou "down", sempre um estado REAL confirmado pela SEFAZ, nunca fabricado
cStatCódigo de status cru da SEFAZ (verbatim, ex.: 107 operacional, 108/109 parado)
xMotivoMensagem cru da SEFAZ (verbatim)
ambiente"producao" ou "homologacao", ambiente monitorado
checkedAtTimestamp 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:

HeaderConteúdo
X-Webhook-Signaturesha256=<hash>
X-Webhook-EventO type do evento (ex.: invoice.authorized)
X-Webhook-DeliveryID da tentativa de entrega

Use exatamente o nome de header da tabela acima.

typescript
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ê:

bash
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

typescript
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:

TentativaAguardaTotal desde o evento
imediato0s
5 minutos5min
30 minutos35min
2 horas~2h35min
24 horas~26h35min

Após a 5ª tentativa falhar, o evento é movido para a Dead Letter Queue.

Dead Letter Queue (DLQ)

bash
# 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

bash
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

bash
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:

bash
# Com ngrok
ngrok http 3000

# Use a URL pública do ngrok ao cadastrar o webhook
# Ex: https://abc123.ngrok.io/webhooks/engine

Boas Práticas


Próximos passos