Agent skill
Mercury Webhooks Skill
Receive and verify Mercury webhooks. Use when setting up Mercury business banking webhook handlers, debugging Mercury-Signature verification, or handling transaction and account balance events such as transaction.created, transaction.updated, and checkingAccount.balance.updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill mercury-webhooks
When to Use This Skill
- How do I receive Mercury webhooks?
- How do I verify the
Mercury-Signatureheader? - How do I handle
transaction.created,transaction.updated, or account balance events? - Why does my Mercury webhook handler never see
transaction.createdin the body? - Why is my Mercury webhook signature verification failing?
Essential Code (USE THIS)
Every Mercury delivery carries Mercury-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 (hex) of "<t>.<raw body>", keyed with the endpoint's secret as-is (a plain string, not decoded). Mercury returns secret only when you create the endpoint (POST /webhooks), so save it then.
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyMercury(rawBody, signatureHeader, secret) {
if (!secret || !signatureHeader) return false;
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.trim().split(/=(.*)/s).slice(0, 2))
);
if (!parts.t || !parts.v1) return false;
// Optional replay window, off by default: Hookdeck's retries and replays keep the original timestamp
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.`).update(rawBody).digest('hex');
const a = Buffer.from(parts.v1), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verification (Python)
import hashlib, hmac
def verify_mercury(raw_body: bytes, signature_header: str, secret: str) -> bool:
if not secret or not signature_header:
return False
parts = dict(kv.strip().split("=", 1) for kv in signature_header.split(",") if "=" in kv)
if "t" not in parts or "v1" not in parts:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"].encode(), expected.encode())
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Timestamp window: off by default
Mercury recommends rejecting timestamps older than 5 minutes. Hookdeck retries and replays resend the original timestamp, so a 5-minute window rejects late retries. The examples therefore skip the freshness check unless you set MERCURY_ to a positive number of seconds (default 0 = off). Use the event id for idempotency instead. references/verification.md shows exactly where the window can be dropped or widened.
Dispatch on resourceType and operationType
The event names you subscribe to (eventTypes such as transaction.created) do not appear in the body. The body is the Events API object: id, resourceType, resourceId, operationType, resourceVersion, occurredAt, changedPaths, mergePatch, previousValues. Dispatch on `${resourceType}.${operationType}`, where operationType is create, update, or delete (not created/updated):
Subscribed eventTypes | Body resourceType + operationType |
|---|---|
transaction.created | transaction + create |
transaction.updated | transaction + update |
checkingAccount.balance.updated | checkingAccount + update |
savingsAccount.balance.updated | savingsAccount + update |
treasuryAccount.balance.updated | treasuryAccount + update |
creditAccount.balance.updated | creditAccount + update |
investmentAccount.balance.updated | investmentAccount + update |
The resourceType values come from Mercury's Events API enum. Mercury publishes no balance event body, so log your first delivery of each type to confirm the pair.
See references/overview.md for the payload.
Important Headers
| Header | Description |
|---|---|
Mercury-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>; t is when the webhook was sent |
Environment Variables
MERCURY_WEBHOOK_SECRET=your_webhook_secret_key # secret returned by POST /webhooks
MERCURY_TIMESTAMP_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 mercury --path /webhooks/mercury
Reference Materials
- references/overview.md - Mercury webhook concepts, events, payload
- references/setup.md - Creating the endpoint through the API and saving the secret
- references/verification.md - Signature details, the timestamp window, and debugging