Reference

Webhooks

A score is a point-in-time answer; webhooks are how you learn it changed. Every payload is signed, retries are aggressive, and delivery is at-least-once — build the receiver accordingly.

Event catalogue

EventFires whenAct on it by
screen.deterioratedAn address you screened crosses into a worse bandRe-reviewing the customer or holding withdrawals
screen.sanctions_matchA previously-clear address matches a screened listFreezing immediately — this is a legal fact, not a gradient
screen.batch_completeA batch submission finishesFetching results; the payload carries counts, not rows
credit.health_factor_breachA monitored wallet drops below your thresholdMargin call or collateral request
credit.liquidatedA monitored wallet is liquidated anywhereRepricing the borrower, not just the position
route.settledA payout reaches your configured confirmation depthReleasing the recipient-facing confirmation
route.failedA payout fails on chainRe-quoting. Never blind-retry the same transaction.
treasury.policy_eventA multisig signer, threshold or module changesPaging whoever owns that multisig. Highest-signal event we send.
treasury.exception_openedMovement cannot be matched to a ledger entryAssigning an owner while the context is fresh
coverage.degradedA chain we ingest falls materially behind headDeciding whether to keep serving decisions on that chain

Payload shape

POST /your/endpoint
X-FTAI-Signature: t=1785363500,v1=8f3c…
X-FTAI-Event-Id: evt_7Kq2…
Content-Type: application/json

{
  "id": "evt_7Kq2…",
  "type": "screen.deteriorated",
  "created_at": "2026-07-30T09:12:44Z",
  "data": {
    "chain": "ethereum",
    "address": "0x7a25…f3b1",
    "previous": { "risk_score": 22, "band": "clear" },
    "current":  { "risk_score": 71, "band": "elevated" },
    "new_reasons": [ { "code": "MIXER_PROXIMITY", "weight": 34 } ],
    "as_of_block": 20914773
  }
}

Deterioration events always carry both previous and current, so your receiver never has to hold state to know what changed.

Verify the signature

The signature is an HMAC-SHA256 over {timestamp}.{raw_body} using your endpoint's signing secret. Verify against the raw bytes — parsing and re-serialising the JSON first will change the signature and fail.

// Node — Express, raw body
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.' + rawBody)
    .digest('hex');
  // constant-time compare — never ===
  const ok = crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(parts.v1));
  // reject anything older than 5 minutes
  const fresh = Math.abs(Date.now()/1000 - Number(parts.t)) < 300;
  return ok && fresh;
}
Reject stale timestamps. Signature verification without a freshness window leaves you open to replay of a legitimately-signed event.

Delivery semantics

Managing endpoints

POST   /v1/webhooks              // create, returns signing secret once
GET    /v1/webhooks              // list, with recent failure counts
PATCH  /v1/webhooks/{id}         // change url or event subscriptions
POST   /v1/webhooks/{id}/test    // send a synthetic event of any type
DELETE /v1/webhooks/{id}

Register more than one endpoint if different events belong to different teams — sanctions matches to your compliance queue, policy events to the treasury pager. Subscribing one endpoint to everything and fanning out internally works, but it makes the paging rules someone else's problem.