# Moneybird Webhooks

## When to Use This Skill

* How do I receive Moneybird webhooks?
* How do I verify the `Moneybird-Signature` header?
* How do I handle `sales_invoice_created`, `sales_invoice_state_changed_to_paid`, or `contact_created`?
* How do I verify a signature with several `v1=` values during a secret rotation?
* Why is my Moneybird webhook signature verification failing?

## Essential Code (USE THIS)

Every Moneybird delivery carries `Moneybird-Signature: t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 (hex) of `"{t}.{raw_request_body}"`, keyed with the webhook's `secret` **as-is**. The API returns `secret` only in the response to `POST /{administration_id}/webhooks.json`; you can reveal it later on the webhook's detail page in Moneybird (after an MFA confirmation). During a secret rotation the header carries one `v1=` per active secret, so accept the request if **any** `v1` matches.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyMoneybird(rawBody, signatureHeader, secret) {
  if (!secret || !signatureHeader) return false;
  let t; const v1 = [];
  for (const part of signatureHeader.split(',')) {          // trim: "t=1, v1=..." is allowed
    const i = part.indexOf('=');
    const key = part.slice(0, i).trim(), value = part.slice(i + 1).trim();
    if (i > 0 && key === 't' && t === undefined) t = value;
    else if (i > 0 && key === 'v1') v1.push(value);          // ignore unknown schemes
  }
  if (!t || !/^\d+$/.test(t) || v1.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.`).update(rawBody).digest('hex');
  // 64-hex check first: Buffer.from(x, 'hex') silently truncates invalid hex
  return v1.reduce((ok, d) => (/^[0-9a-f]{64}$/i.test(d) &&
    crypto.timingSafeEqual(Buffer.from(d.toLowerCase()), Buffer.from(expected))) || ok, false);
}

```

### Verification (Python)

```python
import hashlib, hmac, re

def verify_moneybird(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not secret or not signature_header:
        return False
    t, v1 = None, []
    for part in signature_header.split(","):
        key, sep, value = part.partition("=")
        key, value = key.strip(), value.strip()
        if sep and key == "t" and t is None:
            t = value
        elif sep and key == "v1":
            v1.append(value)
    if not t or not (t.isascii() and t.isdigit()) or not v1:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest().encode()
    # bytes on both sides: a non-ASCII header value is a 400, not a TypeError/500
    return any(re.fullmatch(r"[0-9a-f]{64}", d, re.I) and hmac.compare_digest(d.lower().encode(), expected)
               for d in v1)

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/moneybird-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/moneybird-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/moneybird-webhooks/examples/fastapi/).

### Timestamp window: off by default

Moneybird recommends rejecting a request whose `t` is more than 5 minutes from your clock. **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 `MONEYBIRD_TIMESTAMP_TOLERANCE_SECONDS` to a positive number of seconds (default `0` = off). Moneybird itself retries a non-200 response 10 times with gradually growing intervals, so the same applies without Hookdeck. Use the `Idempotency-Key` header for deduplication instead. [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/moneybird-webhooks/references/verification.md) shows exactly where the window can be dropped or widened.

## Dispatch on `action`

The body is JSON with `administration_id`, `webhook_id`, `webhook_token`, `entity_type` (for example `SalesInvoice`), `entity_id`, `state`, `action`, and `entity` (the same JSON the API returns for that record). Dispatch on `action`, which is one of the event names you put in `enabled_events`:

| `action` | Triggered when |
| --- | --- |
| `sales_invoice_created` | A sales invoice is created |
| `sales_invoice_state_changed_to_paid` | A sales invoice becomes paid |
| `sales_invoice_state_changed_to_late` | A sales invoice becomes late |
| `contact_created` | A contact is created |
| `contact_changed` | A contact is updated |
| `payment_registered` | A payment is registered |

If you subscribe to no events, Moneybird falls back to the legacy behaviour (sales invoice state changes only). A group name such as `estimate` subscribes to every event in that group. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/moneybird-webhooks/references/overview.md) for the full event families and the payload.

## Important Headers

| Header | Description |
| --- | --- |
| `Moneybird-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>[,v1=<hex>...]`; `t` is the delivery timestamp |
| `Idempotency-Key` | Unique per push; the same value is sent on retries, so use it to dedupe |

## Environment Variables

```bash
MONEYBIRD_WEBHOOK_SECRET=your_webhook_secret      # `secret` returned by POST /{administration_id}/webhooks.json
MONEYBIRD_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 moneybird --path /webhooks/moneybird

```

## Reference Materials

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