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-Signature header?
  • How do I handle transaction.created, transaction.updated, or account balance events?
  • Why does my Mercury webhook handler never see transaction.created in 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_TIMESTAMP_TOLERANCE_SECONDS 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 eventTypesBody resourceType + operationType
transaction.createdtransaction + create
transaction.updatedtransaction + update
checkingAccount.balance.updatedcheckingAccount + update
savingsAccount.balance.updatedsavingsAccount + update
treasuryAccount.balance.updatedtreasuryAccount + update
creditAccount.balance.updatedcreditAccount + update
investmentAccount.balance.updatedinvestmentAccount + 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

HeaderDescription
Mercury-Signaturet=<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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →