Webhooks
Receba notificações em tempo real quando a Receita Federal processar uma NFS-e.
Como funciona
Ao emitir uma nota com o campo webhook_url, a API entrega um POST para a URL configurada assim que a Receita responde. O payload inclui uma assinatura HMAC-SHA256 para verificar autenticidade.
Fluxo:
- Você envia
POST /v1/nfsecomwebhook_url - API responde
202imediatamente - Worker processa em background e consulta a Receita
- Quando autorizada/rejeitada, API entrega
POSTpara sua URL - Sua aplicação responde
200para confirmar recebimento
Eventos disponíveis
nfse.autorizadaNota emitida com sucesso. Contém número NFS-e, código de verificação e URLs de PDF/XML.
nfse.rejeitadaNota rejeitada pela Receita. Contém código e descrição do erro.
nfse.canceladaNota cancelada com sucesso após autorização.
Payload
JSON — nfse.autorizada
{
"event": "nfse.autorizada",
"nota_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "AUTORIZADA",
"numero_nfse": "000123",
"codigo_verificacao": "ABC12345",
"pdf_url": "https://api.emitirnotafacil.com.br/v1/nfse/550e.../pdf",
"xml_url": "https://api.emitirnotafacil.com.br/v1/nfse/550e.../xml",
"emitida_em": "2026-04-26T14:30:00Z",
"signature": "sha256=a1b2c3d4e5f6..."
}JSON — nfse.rejeitada
{
"event": "nfse.rejeitada",
"nota_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "REJEITADA",
"erro_codigo": "E10",
"erro_descricao": "CNPJ do tomador inválido",
"signature": "sha256=a1b2c3d4e5f6..."
}Verificação da assinatura
Todo payload inclui o header X-Nota-Signature com o HMAC-SHA256 do body cru. Valide com tempo constante para evitar timing attacks.
Node.js / TypeScript
import crypto from 'crypto'
function verifySignature(rawBody: string, signature: string, secret: string) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
// Comparação em tempo constante — evita timing attack
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
)
}
app.post('/webhooks/nfse', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-nota-signature'] as string
if (!verifySignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Assinatura inválida' })
}
const { event, nota_id, status } = JSON.parse(req.body.toString())
// processe o evento...
res.sendStatus(200)
})Python
import hmac, hashlib
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post('/webhooks/nfse')
def webhook(request: Request):
sig = request.headers.get('x-nota-signature', '')
body = request.body()
if not verify_signature(body, sig, os.getenv('WEBHOOK_SECRET')):
return Response(status_code=401)
payload = json.loads(body)
# processe o evento...
return Response(status_code=200)Política de retry
Se o seu endpoint retornar um status diferente de 2xx, ou não responder em 10 segundos, a API tenta novamente com backoff:
1ª
imediato
2ª
1 minuto
3ª
5 minutos
4ª
30 minutos
5ª
2 horas
Após 5 tentativas sem sucesso, o campo webhook_entregue permanece false. Você pode reprocessar manualmente consultando GET /v1/nfse/:id.
Testando webhooks no sandbox
Use o endpoint de webhook do próprio sandbox para inspecionar payloads sem infraestrutura:
"webhook_url": "https://api.emitirnotafacil.com.br/v1/sandbox/webhook"
Veja os últimos 20 payloads recebidos com GET /v1/sandbox/webhook, ou use o playground →