Webhooks
Register an endpoint from the Webhooks page in an app you have Developer permissions on. You'll
get back a signing secret (whsec_…) — store it, it's shown only once.
Events
An event name is its internal Domain.Entity.Action name, lowercased verbatim — no separators are
inserted, so a multi-word action like Order.ApprovalConfirmed becomes order.approvalconfirmed,
not order.approval_confirmed. When adding an endpoint, the Webhooks page lists exactly the event
prefixes scoped to whichever apps you hold Developer access on.
The order lifecycle (created, submitted, approved, rejected, broadcast, confirmed, failed) is
shared by Payments, Trade, Earn, and Borrow and published as order.* events. Each carries a
domain payload field (payment | trade | earn | borrow) saying which vertical the order belongs
to, and delivery is scoped: a webhook created from an app's Developer page only receives order.*
events for that app's own domain. Vertical-specific facts keep their own names — payment.settled,
payment.received, invoice.*, trade.cancelled, alerts.
See the Events reference for the full, always-current list of every event the platform can emit, generated from the event catalog — including which are forwarded as webhooks and their payload fields.
order.confirmed fires for MARKET and EXCHANGE trade orders. LIMIT orders (routed through CoW
Protocol) don't publish it yet — poll getTradeOrder/listTradeOrders for a LIMIT order's fill
status instead.
Payload
Every delivery is a POST with a JSON body and three headers:
| Header | Description |
|---|---|
X-Treasury-Event | The event name, e.g. payment.submitted |
X-Treasury-Signature | sha256=<hex hmac> — see verification below |
X-Treasury-Delivery | A unique delivery ID, for idempotent processing / retries |
Verify signatures
Signatures are an HMAC-SHA256 of the raw request body, keyed with your endpoint's secret:
import { createHmac, timingSafeEqual } from 'node:crypto';
function isValidSignature(rawBody: string, header: string, secret: string): boolean {
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
const a = Buffer.from(header);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Always compute the HMAC over the raw request body (before any JSON parsing) — most frameworks need an explicit raw-body middleware/route config for the webhook endpoint to make that available.
Failed deliveries are retried with exponential backoff. Delivery status per event is visible on the Webhooks page.