Ir para o conteúdo principal

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:

  1. Você envia POST /v1/nfse com webhook_url
  2. API responde 202 imediatamente
  3. Worker processa em background e consulta a Receita
  4. Quando autorizada/rejeitada, API entrega POST para sua URL
  5. Sua aplicação responde 200 para confirmar recebimento

Eventos disponíveis

nfse.autorizada

Nota emitida com sucesso. Contém número NFS-e, código de verificação e URLs de PDF/XML.

nfse.rejeitada

Nota rejeitada pela Receita. Contém código e descrição do erro.

nfse.cancelada

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

imediato

1 minuto

5 minutos

30 minutos

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 →