Webhooks
Em vez de fazer polling até um estado terminal, você pode receber a conclusão da
emissão por push. A NFE.io entrega um webhook; seu endpoint verifica a
assinatura e reage ao evento. Este guia cobre a verificação canônica via
Nfe\Webhook.
Como a NFE.io assina as entregas
Cada entrega traz o cabeçalho X-Hub-Signature com um HMAC-SHA1 calculado sobre
os bytes exatos do corpo da requisição, no formato sha1=<40 hex>:
X-Hub-Signature: sha1=BCD17C02B9E3B40A18E745E7E04247E4AD2DD935
A API de verificação
A verificação vive na classe estática Nfe\Webhook — ela não precisa de um
Nfe\Client, não lê configuração e não faz nenhuma chamada de rede.
Nfe\Webhook ≠ $nfe->webhooks$nfe->webhooks é o CRUD de webhooks (criar/listar/atualizar alvos de
entrega — veja o cookbook). A verificação de
assinatura é a classe estática Nfe\Webhook; não procure
$nfe->webhooks->verifySignature() — não existe.
use Nfe\Webhook;
$ok = Webhook::verifySignature(
payload: file_get_contents('php://input'),
signature: $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '',
secret: $_ENV['NFE_WEBHOOK_SECRET'],
);
verifySignature(): bool
Retorna true somente quando o HMAC-SHA1 confere exatamente. A comparação é
feita em tempo constante (hash_equals()), o prefixo sha1= é opcional (hex
puro também é aceito) e a comparação de algoritmo/hex é case-insensitive. Um
quarto argumento $algo (padrão 'sha1') permite outro algoritmo — um header
sha256= contra $algo = 'sha1' é rejeitado (sem downgrade).
verifySignature() nunca lança. Entrada ausente, malformada, com algoritmo
errado ou conteúdo não hexadecimal resulta em false.
constructEvent(): Nfe\WebhookEvent
Quando você quer não só validar, mas também desempacotar o evento, use
constructEvent(). Ele verifica primeiro; em caso de falha de assinatura ou
de JSON inválido, lança Nfe\Exception\SignatureVerificationException.
use Nfe\Webhook;
$event = Webhook::constructEvent(
payload: $raw,
sigHeader: $signature,
secret: $_ENV['NFE_WEBHOOK_SECRET'],
);
$event->type; // ex.: "invoice.issued" (a chave `action` do envelope v2)
$event->data; // array com o payload (a chave `payload` do envelope)
$event->id; // id estável para deduplicação, ou null
$event->createdAt; // timestamp ISO-8601 da entrega, ou null
Nfe\WebhookEvent é um objeto de valor imutável (final readonly). Se o corpo
carrega o envelope v2 { action, payload }, ele é desembrulhado: type recebe
action e data recebe o payload interno. Sem envelope, o helper procura um
campo type/event_type/action no nível raiz.
Leia o corpo CRU antes de parsear o JSON
A NFE.io assina os bytes que entregou. Leia o corpo cru antes de qualquer parsing de JSON e passe esses bytes ao verificador.
Nunca passe um array já parseado e re-serializado (por exemplo
json_encode($payload)). A ordem das chaves e os espaços em branco vão diferir
dos bytes assinados, e a verificação falhará de forma imprevisível. Sempre use o
corpo cru — file_get_contents('php://input') (ou $request->getContent() no
Laravel/Symfony).
Exemplo: endpoint em PHP puro
<?php
use Nfe\Exception\SignatureVerificationException;
use Nfe\Webhook;
require __DIR__ . '/vendor/autoload.php';
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '';
try {
$event = Webhook::constructEvent($raw, $signature, $_ENV['NFE_WEBHOOK_SECRET']);
} catch (SignatureVerificationException) {
http_response_code(401);
exit;
}
// Dedupe pelo id do evento/nota antes de aplicar efeitos colaterais.
processarEventoIdempotente($event->type, $event->data, $event->id);
http_response_code(200);
Use $request->getContent() como corpo cru e
$request->header('X-Hub-Signature') como assinatura, e exclua a rota do
middleware de CSRF (webhooks não têm token CSRF).
Validade não é frescor — handlers devem ser idempotentes
A NFE.io não envia primitiva de anti-replay: as entregas trazem apenas o HMAC-SHA1 sobre o corpo, sem timestamp e sem nonce. Uma assinatura válida prova autenticidade, mas não frescor — uma entrega reproduzida (replay) carrega uma assinatura perfeitamente válida.
Trate seus handlers como idempotentes e faça deduplicação pelo
$event->id (id do evento ou da nota). Processar a mesma entrega duas vezes
não pode gerar efeitos colaterais duplicados.
Próximos passos
- Cookbook de webhooks — CRUD dos alvos de entrega.
- Emissão assíncrona e polling — polling como alternativa ao push.
- Tratamento de erros —
SignatureVerificationExceptionna hierarquia.