Agent skill
Modern Treasury Webhooks Skill
Receive and verify Modern Treasury webhooks. Use when setting up Modern Treasury webhook handlers, debugging X-Signature verification (hex HMAC-SHA256 of the raw body), routing on the X-Topic header and event field, or handling payment operations events like payment_order.completed, payment_order.failed, expected_payment.reconciled and incoming_payment_detail.completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill modern-treasury-webhooks
When to Use This Skill
- How do I receive Modern Treasury webhooks?
- How do I verify a Modern Treasury webhook signature (
X-Signature)? - Why is my Modern Treasury webhook signature verification failing?
- How do I tell a
payment_orderevent from anexpected_paymentevent? (X-Topicheader plus bodyevent) - How do I handle
payment_order.completed,payment_order.failedorpayment_order.reconciled? - How do I avoid processing the same Modern Treasury webhook twice (
X-Webhook-ID)?
Verification (core)
X-Signature is the lowercase hex HMAC-SHA256 of the raw request body, keyed with your webhook key (Developers > Webhooks) used as a plain string. There is no timestamp and no prefix.
const crypto = require('crypto');
function verifySignature(rawBody, signature, webhookKey) {
if (!signature || !webhookKey) return false;
const expected = crypto.createHmac('sha256', webhookKey).update(rawBody).digest('hex');
const received = Buffer.from(signature.trim().toLowerCase());
const expectedBuf = Buffer.from(expected);
return received.length === expectedBuf.length &&
crypto.timingSafeEqual(received, expectedBuf);
}
// Node lowercases header names: read req.headers['x-signature'], not 'X-Signature'
Verify against the raw body bytes, before any JSON parsing. In Express use express.raw({ type: '*/*' }).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
The official SDKs (modern-treasury on npm and PyPI) expose webhooks.validateSignature / validate_signature, but they compare with a plain ===/== and need a configured API client, so the examples verify manually with a timing-safe compare.
Request Headers and Body
| Header | Meaning |
|---|---|
X-Signature | Hex HMAC-SHA256 of the body |
X-Topic | Object category, e.g. payment_order, expected_payment |
X-Webhook-ID | Unique per webhook, stable across retries (use for idempotency) |
X-Event-ID | ID of the Event object |
X-Delivery-ID | Changes on every delivery attempt |
X-Live-Mode | true for live traffic, false for test |
The body is { "event": "completed", "data": { ...the object... } }. The event is the bare event name; combine it with X-Topic (payment_order.completed) to route.
Common Event Types
Events are named per topic. Verified against the Modern Treasury docs:
| Topic | Events |
|---|---|
payment_order | created, updated, approved, denied, cancelled, begin_processing, finish_processing, acknowledged, confirmed, tentatively_reconciled, completed, reconciled, returned, failed, redrafted, reversed, held, resolved |
expected_payment | created, reconciled, partially_reconciled, tentatively_reconciled, unreconciled, overdue, archived |
incoming_payment_detail | created, tentatively_reconciled, completed, reconciled, returned, failed, return_failed, unreconciled |
transaction | created, reconciled, updated |
return | created, begin_processing, finish_processing, reconciled, completed, returned, failed, unreconciled |
reversal | created, begin_processing, finish_processing, completed, failed, returned, unreconciled |
Other topics: balance_report, case, connection_legal_entity, decision, external_account, internal_account, ledger_, ledger_transaction, legal_entity, user_onboarding. See references/overview.md for the full list.
Environment Variables
MODERN_TREASURY_WEBHOOK_KEY=your_webhook_key_here
Local Development
npx hookdeck-cli listen 3000 modern-treasury --path /webhooks/modern-treasury
No account required: the CLI creates a guest account on first run and provides a local tunnel plus a web UI for inspecting requests. Use the printed URL as your endpoint URL in Modern Treasury (sandbox accepts localhost URLs directly; production requires HTTPS). Use port 8000 for the FastAPI example.
Gotchas
- Raw body only. Re-serialized JSON changes the bytes and breaks the signature.
- Lowercase header lookup in Node. The Modern Treasury docs sample reads
req.headers['X-Signature'], which is alwaysundefined. - Respond within 5 seconds with a 2xx. Otherwise it is retried with exponential backoff, up to 15 attempts; the endpoint is then paused. Acknowledge fast and process async.
- Duplicates happen. Retries reuse
X-Webhook-ID, and a redrafted payment order can emit the same event again for the sameid. completedis notreconciled. Treat a payment order as fully settled atreconciled.- Same URL for live and test? Branch on
X-Live-Mode. - Ignore
X-Internal-Signature. It is for Modern Treasury's own use.
Reference Materials
- references/overview.md - Topics, events, headers and payload structure
- references/setup.md - Add a webhook endpoint and find the webhook key
- references/verification.md - Signature details, SDK notes, debugging