SonaCORE

Eventos e webhooks

Ao menos uma vez, identificado de forma determinística, assinado.

Toda mudança de estado que vale reagir é publicada como um evento versionado, e um webhook entrega o envelope do evento a você byte a byte, como ele foi assinado. As três coisas para acertar são deduplicação, verificação da assinatura contra os bytes crus, e não esperar uma ordenação que não foi prometida.

O envelope

Uma forma para todo tipo de evento. O payload mora em data; todo o resto é endereçamento.

obligation.settled
{
  "event_id": "evt_9f2c8a41b7d64e02a3c5f0e81d97b6aa",
  "event_type": "obligation.settled",
  "event_version": 1,
  "occurred_at": "2026-07-15T12:00:00Z",
  "published_at": "2026-07-15T12:00:01Z",
  "tenant": "sandbox",
  "entity": { "entity_id": "obl_a1b2…", "version": 3 },
  "correlation": {
    "obligation_id": "obl_a1b2…",
    "sqd_id": null,
    "swp_id": null,
    "tx_id": "abc123_0"
  },
  "data": { "…": "específico do tipo" }
}
CampoObservações
event_idCasa com ^evt_[0-9a-f]{32}$. Determinístico, então uma reentrega carrega o mesmo id. Esta é a sua chave de deduplicação.
event_versionSempre 1 hoje. Mudanças dentro da versão 1 são apenas aditivas, então ignore campos e tipos que você não reconhece em vez de rejeitá-los.
occurred_atQuando a mudança aconteceu. É este que você usa para ordenar e raciocinar.
published_atQuando emitimos. Não faz parte da identidade do evento; muda em uma reentrega.
correlationAs quatro chaves estão sempre presentes, nulas onde não se aplicam, para você poder cruzar sem checar antes se a chave existe.
dataEspecífico do tipo, e nunca uma imagem crua de registro. Nenhum campo aqui carrega dado pessoal.

Semântica de entrega

Eventos também são navegáveis pela API em uma janela recente e limitada, que é a ferramenta certa para se atualizar depois de uma queda e a ferramenta errada para uma trilha de auditoria.

navegar
GET /v1/events?type=obligation.settled&entity_id=obl_a1b2…&limit=50

Verificando um webhook

Cada entrega carrega um HMAC com data e hora. Três detalhes causam quase toda integração que falha, então estão escritos sem rodeio:

CabeçalhoCarrega
X-Quadra-Signaturet=<segundos unix>,v1=<digest hex>. Esquemas futuros adicionam componentes em vez de substituir v1, então faça parse dos pares separados por vírgula e leia o que você suporta.
X-Quadra-Event-IdO mesmo valor de event_id no corpo.
X-Quadra-Event-TypePara rotear sem fazer parse do corpo.
X-Quadra-Webhook-IdPara qual dos seus endpoints isto foi.
X-Quadra-Delivery-IdEsta tentativa. Muda em uma repetição, diferente do id do evento.
User-Agentquadra-webhooks/1
python
import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, header: str, tolerance_s: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance_s:
        return False  # janela de repetição: rejeita qualquer coisa com mais de cinco minutos
    expected = hmac.new(secret.encode(), b"%d." % t + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Vetores de teste

A sua implementação tem que reproduzir estes exatamente. Os mesmos vetores são conferidos contra o nosso próprio assinador na nossa suíte de testes, então a documentação e o código não podem se afastar um do outro. Se o segundo falhar e os outros passarem, o seu problema é codificação de caracteres.

vetor 1 — envelope canônico
secret: whsec_cXVhZHJhLXdlYmhvb2tzLXRlc3QtdmVjdG9yLTAwMDE
t:      1784116800
body:   {"correlation":{"obligation_id":"obl_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6","sqd_id":null,"swp_id":null,"tx_id":"abc123_0"},"data":{"amount":100,"creditor_account_id":"acct_creditor001","currency":"QBRL","debtor_account_id":"acct_debtor001","obligation_id":"obl_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6","rail_family":"QUADRA","resolution_outcome":"SETTLED","resolved_at":"2026-07-15T12:00:00Z","state":"FINAL","tx_id":"abc123_0"},"entity":{"entity_id":"obl_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6","pk":"abc123#0","sk":"OBLIGATION","table":"quadra-utxos","version":3},"event_id":"evt_9f2c8a41b7d64e02a3c5f0e81d97b6aa","event_type":"obligation.settled","event_version":1,"occurred_at":"2026-07-15T12:00:00Z","published_at":"2026-07-15T12:00:01Z","tenant":"sandbox"}

expected: t=1784116800,v1=09b08259917f36a14dbba2c3450d9cd34cc6985ff14de579221502d3f6a4c7f6
vetor 2 — utf-8 fixa a codificação
secret: whsec_cXVhZHJhLXdlYmhvb2tzLXRlc3QtdmVjdG9yLTAwMDE
t:      1784116801
body:   {"memo":"café ☕"}

expected: t=1784116801,v1=60532e477af0e7b8a768d28b5d197854003ee69c27cd7086788326b5df26c73d
vetor 3 — corpo mínimo
secret: whsec_cXVhZHJhLXdlYmhvb2tzLXRlc3QtdmVjdG9yLTAwMDE
t:      1784116802
body:   {}

expected: t=1784116802,v1=747a7a33ce53cce185ddc76e615260a22ebb98fcff1cf0f80dfc672c0a1530ef
vetor 4 — outro secret, o mesmo corpo
secret: whsec_cXVhZHJhLXdlYmhvb2tzLXRlc3QtdmVjdG9yLTAwMDI
t:      1784116803
body:   {}

expected: t=1784116803,v1=a56ca4fb427e536918111704086135febfabc2ac54e23815ffc7d1ff03cc0e43

Secrets

Um secret é whsec_ seguido de 43 caracteres base64url. Ele é devolvido em texto claro exatamente uma vez, na resposta que cria o endpoint, e mascarado até os quatro últimos caracteres em todo lugar depois disso. Não existe endpoint de rotação: rotacionar é apagar o endpoint e criar um novo, o que força o secret novo a ser tratado de propósito em vez de ser recolhido em silêncio.

Repetições e suspensão

Uma entrega tem sucesso em qualquer 2xx dentro de dez segundos. Um 3xx é falha, não um redirecionamento a seguir. Nunca seguimos um, porque um redirecionamento em uma entrega assinada é um pedido para mandar o seu payload para outro lugar.

TentativaEspera
1imediataA primeira tentativa.
21 minutoCobre um restart ou uma oscilação curta.
35 minutosCobre um deploy.
4 – 815 minutos cadaDepois disso a entrega é arquivada como falha.

Vinte entregas arquivadas em sequência suspendem o endpoint automaticamente. Enquanto ele está suspenso nada é enviado, e reativá-lo não repõe o que foi perdido. Atualize-se pelo endpoint de navegação de eventos.

Endpoints têm que ser alcançáveis e públicos
O registro exige HTTPS, e o destino é reconferido contra uma lista de bloqueio de endereços internos antes de cada tentativa, e não só no momento do registro, então um hostname que depois passa a resolver para dentro deixa de receber entregas. Existe também um limite de quantos endpoints podem estar ativos ao mesmo tempo.

Testando um endpoint

Uma entrega de teste ignora os seus filtros, carrega test: true, e nunca é persistida, então procurá-la depois na navegação de eventos corretamente devolve 404. Isso é deliberado: um evento de teste não faz parte do histórico do seu ledger.