Documentação da API

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.

Existe um botão de evento de teste. Use antes de contar com o destino: endereço nunca testado é a causa mais comum de entrega que nunca chega.

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

EventoQuando chega
ebl.document.completedA leitura terminou, com sucesso ou com falha
ebl.document.reviewedUma 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.

O corpo de 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.

Node
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);
}
Python
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))
Assine sobre o corpo bruto, antes de qualquer parse. Serializar o JSON de novo reordena chaves e muda espaços, e aí a assinatura não bate.

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.

TentativaEsperaAcumulado
11 min1 min
34 min7 min
632 min1h03
94h168h31
1117h0434h07

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.