Receber por webhook
Em vez de perguntar de tempos em tempos se ficou pronto, deixe o resultado chegar. O corpo entregue é igual ao da consulta, assinado. Funciona da mesma forma em todos os produtos.
Configurar
No portal, na tela de Entregas do produto. Para o eBL, em eBL › Entregas. É um destino por produto, então cada um pode apontar para um sistema diferente da sua empresa.
O destino precisa ser https e apontar para um endereço público. Endereços de rede interna são recusados.
O que chega
POST /seu-endpoint
content-type: application/json
comexdoc-event: ebl.document.completed
comexdoc-delivery: 6fbb2659-6e9f-4f00-a211-dd1a7fef2dda
comexdoc-signature: t=1788652632,v1=8a3f...c91
{
"schema_version": "2.0",
"job_id": "6fbb2659-6e9f-4f00-a211-dd1a7fef2dda",
"document_hash": "efca21ed...",
"status": "completed",
"data": { ... },
"error_code": null,
"human_corrected_fields": [],
"billed_amount_brl": "1.0000",
"original_file_url": "/v1/ebl/documents/6fbb2659-.../file"
}O cabeçalho comexdoc-event diz o que aconteceu, e começa com o nome do produto (ebl.). Um mesmo servidor pode receber de vários produtos e ramificar por ele.
Toda leitura concluída notifica, inclusive quando falha. Do seu lado, "não chegou nada" e "chegou dizendo que falhou" são situações diferentes, e só a segunda dá para tratar.
Os eventos do eBL
| Evento | Quando chega |
|---|---|
ebl.document.completed | A leitura terminou, com sucesso ou com falha |
ebl.document.reviewed | Uma pessoa conferiu a leitura, tendo corrigido campos ou apenas confirmado |
O mesmo job_id pode chegar duas vezes: uma quando a máquina termina, outra quando alguém confere. São entregas diferentes com corpos diferentes, e o cabeçalho comexdoc-event é o que separa as duas.
reviewed substitui o de completed. Ele traz o documento já com as correções aplicadas, e corrected_fields lista o que uma pessoa alterou. Se o seu sistema guardou o primeiro corpo, o segundo é quem passa a valer.A conferência dispara a entrega do job em que ela foi feita. Releituras do mesmo BL mudam de estado junto, mas não geram uma entrega cada: a decisão humana foi uma só. Para correlacionar, use o document_hash, que é igual em todas.
Conferência não é cobrada, e a entrega dela também não.
Confira a assinatura
O cabeçalho comexdoc-signature traz t, um instante Unix, e v1, o HMAC-SHA256 de "<t>.<corpo bruto>" com o segredo do seu destino. O carimbo de tempo entra dentro da assinatura, então uma entrega capturada não pode ser reapresentada depois com carimbo novo.
import { createHmac, timingSafeEqual } from "node:crypto";
function conferir(corpoBruto, cabecalho, segredo) {
const t = /t=(\d+)/.exec(cabecalho)?.[1];
const v1 = /v1=([0-9a-f]+)/.exec(cabecalho)?.[1];
if (!t || !v1) return false;
// Recusa entrega antiga.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const esperado = createHmac("sha256", segredo)
.update(`${t}.${corpoBruto}`)
.digest("hex");
const a = Buffer.from(esperado, "hex");
const b = Buffer.from(v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}import hmac, hashlib, re, time
def conferir(corpo_bruto: bytes, cabecalho: str, segredo: str) -> bool:
t = re.search(r"t=(\d+)", cabecalho)
v1 = re.search(r"v1=([0-9a-f]+)", cabecalho)
if not t or not v1:
return False
if abs(time.time() - int(t.group(1))) > 300:
return False
esperado = hmac.new(
segredo.encode(),
t.group(1).encode() + b"." + corpo_bruto,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(esperado, v1.group(1))Responda 2xx, e responda rápido
Qualquer 2xx encerra a entrega. Qualquer outra coisa conta como falha e agenda a próxima tentativa, incluindo 5xx, 4xx, tempo esgotado e conexão recusada. O limite é de 10 segundos, então guarde o corpo, responda e processe depois.
Deduplique pelo job_id. A mesma entrega pode chegar duas vezes se a sua resposta se perder no caminho de volta.
Quando falha
A espera dobra a cada tentativa, começando em um minuto. As primeiras cabem na primeira hora e resolvem sozinhas o caso comum: um deploy seu, um reinício, uma oscilação de rede. As últimas se espalham por mais um dia e meio.
| Tentativa | Espera | Acumulado |
|---|---|---|
| 1 | 1 min | 1 min |
| 3 | 4 min | 7 min |
| 6 | 32 min | 1h03 |
| 9 | 4h16 | 8h31 |
| 11 | 17h04 | 34h07 |
Esgotadas as tentativas, a entrega fica na fila da conta, visível no portal, até alguém agir. Reenvio manual é uma tentativa só e não reinicia a escala. Reenvio não é cobrado.
Pela API, a mesma fila está em GET /v1/ebl/webhooks, com POST /v1/ebl/webhooks/{job_id}/resend e POST /v1/ebl/webhooks/resend-all.
Se o webhook e a consulta divergirem
A consulta está certa. O corpo entregue é congelado quando a leitura termina, para que um reenvio entregue o mesmo conteúdo da primeira tentativa. O valor de agora está sempre no GET.