Webhooks
Cada mudança de status de um documento gera um evento, enviado como POST JSON para a URL da sua empresa. Use o webhook em vez de consultar em laço: quando há conferência humana, a liberação pode levar horas.
Configurar
O proprietário cadastra a URL em Webhook, no portal: HTTPS, porta 443 e endereço público. A equipe finstat aprova o destino antes da primeira entrega; trocar a URL pede nova aprovação. O segredo de assinatura é gerado ali e aparece uma única vez; regerar invalida o anterior na hora.
O evento
POST /finstat/webhook HTTP/1.1
Content-Type: application/json
X-Finstat-Event-Id: 00000000-0000-4000-8000-000000000002
X-Finstat-Signature: sha256=097d31f3450e191b0ced1cd50e4c799405c14fea14c14e6b5c0bfea20b6f61f2
{"document_id":"00000000-0000-4000-8000-000000000001","id":"00000000-0000-4000-8000-000000000002","occurred_at":1767234600,"reason":null,"status":"completed"}| Campo | Descrição |
|---|---|
id | Id do evento, igual ao cabeçalho X-Finstat-Event-Id. |
document_id | Id do documento. |
status | Nova situação. |
reason | Motivo, quando houver; senão null. |
occurred_at | Momento da mudança, em segundos Unix. |
O evento não traz o resultado. Com status completed, busque GET /v1/documents/{id}/result.
Verificar a assinatura
X-Finstat-Signature é sha256= seguido do HMAC-SHA256 (hex) do JSON canônico do corpo, com o segredo do webhook:
- chaves ordenadas;
- separadores
,e:sem espaços; - UTF-8 sem escapes
\u.
Compare em tempo constante. Assinatura ausente ou diferente: responda 400 e não processe.
import express from "express";
import { Finstat } from "finstat";
const finstat = new Finstat(process.env.FINSTAT_API_KEY!);
const app = express();
app.post("/finstat/webhook", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = finstat.webhooks.constructEvent(
req.body, // corpo CRU, sem JSON.parse antes
req.header("X-Finstat-Signature"),
process.env.FINSTAT_WEBHOOK_SECRET!,
);
} catch {
return res.status(400).end();
}
res.status(200).end(); // responda em até 10 s e processe depois
if (event.status === "completed") await queue.add(event.document_id);
});import { createHmac, timingSafeEqual } from "node:crypto";
function canonicalJson(value) {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (value !== null && typeof value === "object") {
const keys = Object.keys(value).sort();
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(",")}}`;
}
return JSON.stringify(value);
}
export function validSignature(rawBody, signature) {
const canonical = canonicalJson(JSON.parse(rawBody));
const expected =
"sha256=" +
createHmac("sha256", process.env.FINSTAT_WEBHOOK_SECRET).update(canonical, "utf8").digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}import hashlib
import hmac
import json
import os
def valid_signature(body: bytes, signature: str) -> bool:
canonical = json.dumps(
json.loads(body), ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
secret = os.environ["FINSTAT_WEBHOOK_SECRET"].encode()
expected = "sha256=" + hmac.new(secret, canonical.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Para testar a sua implementação: com o segredo segredo-de-exemplo, o corpo do exemplo acima produz exatamente a assinatura mostrada no cabeçalho.
Responder e deduplicar
- Responda
2xxem até 10 s e processe depois (fila, job em segundo plano). - A entrega é pelo menos uma vez: deduplique por
X-Finstat-Event-Id(igual aoiddo corpo) antes de agir. - Eventos do mesmo documento chegam em ordem; um evento ainda não entregue segura os seguintes desse documento.
- Redirecionamentos não são seguidos.
Novas tentativas e reenvio
Sem 2xx, são até 12 tentativas no total, com espera de 30 s que dobra até 6 h. Depois disso o evento fica esgotado e continua segurando os seguintes do mesmo documento até ser reenviado pelo portal.
O portal lista as entregas com o status HTTP da última tentativa e permite reenviar um evento. Depois de uma queda longa do seu receptor, reenvie os eventos esgotados para destravar a fila.