finstatdocs
API Entrar

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

Entrega
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"}
CampoDescrição
idId do evento, igual ao cabeçalho X-Finstat-Event-Id.
document_idId do documento.
statusNova situação.
reasonMotivo, quando houver; senão null.
occurred_atMomento 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.

Node (SDK)
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);
});
Node
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);
}
Python
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 2xx em até 10 s e processe depois (fila, job em segundo plano).
  • A entrega é pelo menos uma vez: deduplique por X-Finstat-Event-Id (igual ao id do 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.