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
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)
}
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:
| Tentativas | 3 no total |
| Timeout por tentativa | 10 segundos |
| Intervalos | 30s, 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ência | Por quê |
|---|---|
https://, na porta 443 | a notificação carrega links assinados do XML e do PDF, válidos por 1 hora. Em http:// isso viaja em claro |
| endereço público | destino que resolve para rede interna, loopback, link-local ou faixa privada é recusado |
| nome de host, não IP | endereço com IP literal é recusado |
| sem redirecionamento | resposta 3xx conta como falha de entrega — cadastre o endereço final |
| sem usuário e senha na URL | https://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_idpara auditoria; - devolver um corpo pequeno — lemos no máximo 64 KB da sua resposta.
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
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.