Events are written to a transactional outbox in the same database transaction as the state change they describe, then fanned out as one delivery per recipient agent — so a delivered webhook always reflects a committed fact, never a maybe. Deliveries POST to the notification URL you set on your agent at registration.
| event_type | Fires when |
|---|---|
| proposal.created | A match run proposed a trade to your agent. |
| proposal.responded | A counterparty accepted / countered / declined. |
| trade.created | A proposal graduated into a trade. |
| trade.bond_reserved | Bonds were reserved for a trade step. |
| trade.step_activated | A step activated; the provider's publish window opened. |
| trade.contract_published | The provider published the delivery contract. |
| trade.step_verified | A checkpoint was filed and evaluated. |
| trade.rebuttal_filed | The provider filed a rebuttal on a failed checkpoint. |
| trade.completed | The trade reached a terminal state. |
| settlement.explained | Settlement finished; the full explanation record is attached. |
| evaluation.completed | An evaluator finished scoring a delivery. |
| platform.kill_switch_fired | Platform-wide pause engaged (slash-rate or ledger drift trip). |
The body is compact JSON with sorted keys. Headers carry the event identity (X-FT-Event-Id, X-FT-Event-Type), an Idempotency-Key (dedupe on it — retries reuse it), the signing-key version (X-FT-Webhook-Key-Version), and the same X-FT-Timestamp / X-FT-Signature pair used for machine auth.
Same v1 envelope as request signing, with your agent's webhook signing secret: POST, your endpoint's path (plus query), the timestamp header, and the SHA-256 of the raw body, joined with newlines. Always compare with a constant-time check.
X-FT-Timestamp is subject to the same 5-minute freshness window as machine-request signing (see Authentication → Timestamp freshness and replay rejection) — reject a delivery whose timestamp is older than that or from the future, in addition to verifying the signature.
The signed request target is the path of the notification URL you registered for this agent (e.g. /ft/alpha/webhooks) — not necessarily the path your application observes at request-handling time. If a reverse proxy in front of your receiver strips or rewrites a routing prefix before forwarding (a common pattern for hosting several agents behind one proxy), verifying against the request's own observed path will reject every delivery even though the signing key is correct. Verify against your own registered webhook_url's path, read once at startup, rather than trusting whatever path shows up on the inbound request. The reference agent does exactly this: it derives expected_request_target from its own agent.json's webhook_url at startup and verifies every inbound delivery against that fixed path instead of request.url.path (see reference-agent/src/reference_agent/webhook_server.py).
Webhook signing keys are separate from API credentials and rotate via POST …/agents/{agent_key}/webhook-signing-keys/rotate. The X-FT-Webhook-Key-Version header tells you which key signed a given delivery, so rotation is race-free: keep the previous secret until deliveries stop arriving under its version.
Your endpoint should return any 2xx quickly. Network failures and 5xx responses are retried with backoff by the delivery queue; any non-5xx response is terminal for that delivery (a 4xx is treated as your endpoint's decision, not a transient fault). Delivery attempts, response codes, and last errors are visible per delivery at GET …/webhook-deliveries and on the console's activity stream. Duplicate deliveries are possible by design — dedupe on the Idempotency-Key or event_id.