Pular para o conteúdo principal

Webhooks

Webhooks avisam sua aplicação quando uma operação muda de estado, reduzindo a necessidade de consultas repetidas.

Informe a URL na própria emissão:

{
"cpf_cnpj": "11222333000181",
"webhook_url": "https://seu-sistema.com.br/webhooks/conota",
"nota": { "...": "..." }
}

Fluxo​

1. Seu sistema envia a emissão -> 202 { job_id }
2. O Conota processa
3. O Conota chama o seu webhook (POST, JSON)
4. Seu sistema responde 2xx rapidamente
5. Seu sistema persiste o novo estado

Headers da notificação​

POST /webhooks/conota
Content-Type: application/json
X-Qualyfiscal-Signature: sha256=<hmac_hex>
X-Qualyfiscal-Job-Id: 6f8dc295-...
X-Qualyfiscal-Tentativa: 1
Por que o header não se chama X-Conota-*

Esses nomes são contrato, não marca. O código dos clientes já lê X-Qualyfiscal-Signature para conferir a assinatura; renomear quebraria a validação de toda integração existente — e a falha apareceria do lado deles, silenciosamente. Se um dia mudar, os dois nomes conviverão por bastante tempo antes.

Validando a assinatura​

A assinatura é um HMAC-SHA256 do corpo bruto, em hexadecimal, prefixado por sha256=.

const crypto = require('crypto')

function assinaturaValida(corpoBruto, headerRecebido, segredo) {
const esperado = 'sha256=' + crypto
.createHmac('sha256', segredo)
.update(corpoBruto)
.digest('hex')

const a = Buffer.from(esperado)
const b = Buffer.from(headerRecebido || '')
// timingSafeEqual exige mesmo tamanho — compare o tamanho antes.
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Use o corpo BRUTO, não o JSON reserializado

JSON.parse seguido de JSON.stringify pode reordenar chaves e mudar espaçamento — e a assinatura deixa de bater. Guarde o body cru antes do parse.

Retentativas​

Se o seu endpoint não responder, o Conota tenta de novo:

Tentativas3 no total
Timeout por tentativa10 segundos
Intervalos30s, depois 60s

O número da tentativa vem em X-Qualyfiscal-Tentativa.

O que o endereço precisa cumprir​

Estas não são recomendações: o destino que não atender é recusado antes da conexão, e a notificação não sai. A nota fiscal não é afetada — o que falha é a entrega.

ExigênciaPor quê
https://, na porta 443a notificação carrega links assinados do XML e do PDF, válidos por 1 hora. Em http:// isso viaja em claro
endereço públicodestino que resolve para rede interna, loopback, link-local ou faixa privada é recusado
nome de host, não IPendereço com IP literal é recusado
sem redirecionamentoresposta 3xx conta como falha de entrega — cadastre o endereço final
sem usuário e senha na URLhttps://user:senha@destino é recusado

A recusa fica registrada no histórico do webhook com o motivo, e não é reagendada: destino recusado é falha determinística, e tentar três vezes daria o mesmo "não".

Seu endpoint deve​

  • responder rápido (menos de 10s) e processar o trabalho pesado de forma assíncrona;
  • validar a assinatura antes de confiar no conteúdo;
  • ser idempotente — o mesmo evento pode chegar mais de uma vez;
  • registrar o job_id para auditoria;
  • devolver um corpo pequeno — lemos no máximo 64 KB da sua resposta.
Contas criadas a partir de 05/09/2026 cadastram o destino antes

Nessas contas, o endereço precisa estar cadastrado no painel, em Integração → Destinos de webhook, e o webhook_url da emissão precisa ser igual a um cadastro ativo do mesmo ambiente.

Se não estiver, a emissão é recusada com 422 webhook_destino_nao_autorizado — antes de emitir e sem consumir cota. Nenhuma nota é gerada.

Contas anteriores a essa data seguem como sempre: o webhook_url é livre, e só as exigências de endereço acima valem.

Webhook não substitui consulta​

Uma notificação perdida não pode travar a sua operação

Se o seu servidor estiver fora do ar durante as três tentativas, a notificação se perde. Sua aplicação precisa conseguir reconciliar o estado mesmo assim.

Mantenha uma rotina de recuperação que varra jobs sem estado final e consulte POST /v1/jobs/consultar-lote. Webhook é o caminho rápido; a consulta é a rede de segurança.