# 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)

```javascript
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)

```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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mercury-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mercury-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mercury-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/mercury-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/mercury-webhooks/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

```bash
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

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 mercury --path /webhooks/mercury

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mercury-webhooks/references/overview.md) - Mercury webhook concepts, events, payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mercury-webhooks/references/setup.md) - Creating the endpoint through the API and saving the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mercury-webhooks/references/verification.md) - Signature details, the timestamp window, and debugging