# Verifying signatures

Every webhook request Moneybird sends includes a `Moneybird-Signature` header. Use it to verify that the request was sent by Moneybird and that the payload was not modified in transit. Verification is optional but strongly recommended for any production integration.

## The `Moneybird-Signature` header

The header contains a timestamp and one or more signatures, separated by commas:

```
Moneybird-Signature: t=1748534400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

- `t=` — the delivery timestamp as a Unix epoch (seconds).
- `v1=` — an HMAC-SHA256 digest of the signed payload, hex-encoded.

During a [secret rotation](#managing-your-signing-secret) the header carries one `v1=` digest per currently active secret:

```
Moneybird-Signature: t=1748534400,v1=5257a869...,v1=6ffbb59b...
```

Future signing schemes may introduce new prefixes. Ignore any scheme you do not recognise to prevent downgrade attacks.

## How to verify

1. Split the header on `,` and parse each `key=value` pair.
2. Extract `t` and collect every `v1` value.
3. Build the signed payload by concatenating the timestamp, a literal `.`, and the **raw request body** as received on the wire:

   ```
   signed_payload = "{t}.{raw_request_body}"
   ```

   The body must be the exact bytes received. Re-serialising the JSON (for example after parsing and re-encoding) will change byte-for-byte content and break verification.

4. Compute `HMAC-SHA256(signing_secret, signed_payload)` and hex-encode the result.
5. Compare your computed digest to each `v1` value using a constant-time comparison. Accept the request if **any** value matches.
6. Reject the request if `t` is more than 5 minutes away from the current time. The timestamp is part of the signed payload, so an attacker cannot modify it without also invalidating the digest, but a freshness check protects against replay.

## Managing your signing secret

**Retrieving the secret.** When you create a webhook via `POST /{administration_id}/webhooks.json`, the response body includes a `secret` field. This is the only time the secret is exposed through the API — store it in your secret manager immediately. `GET`, `activate`, and `deactivate` responses do not include the `secret` field.

**Recovery and rotation.** If you lose the secret, or want to rotate it after a suspected leak, open the webhook on its detail page inside Moneybird. The secret can be revealed there after an MFA confirmation, and the same page supports rotation. When you rotate, you choose a grace period during which the previous secret remains valid alongside the new one.

**Behaviour during a rotation grace period.** While both secrets are active, every delivery's `Moneybird-Signature` header carries one `v1=` digest per active secret. Recipients that accept *any* matching digest (step 5 above) continue to verify successfully throughout the rollover — first against the old secret, and against the new secret once they have updated their stored value.

**Existing webhooks.** Webhooks created before signing was introduced have been backfilled with a secret, so signed requests are sent for every webhook regardless of when it was registered. To obtain the secret for a pre-existing webhook, reveal it through the webhook detail page in Moneybird.
