# Verify signatures

> Check that a delivery came from Oatmilk and wasn't changed on the way.

Source: https://app.getoatmilk.com/docs/webhooks/signatures

Anyone can send a request to your endpoint, so check every delivery before you trust it. Oatmilk signs each one with your endpoint's secret and puts the signature in the `Oatmilk-Signature` header:

```http
Oatmilk-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

- `t` is when the delivery was signed, in Unix seconds.
- `v1` is an HMAC-SHA256 of the text `{t}.{raw body}`, keyed with your endpoint's secret, as hex.

## Check it

1. Read the **raw body** exactly as it arrived, before any JSON parsing. Parsing and re-serialising changes the bytes, and the signature won't match.
2. Split the header on commas, then each part on the first `=`, to get `t` and `v1`.
3. Refuse the delivery if `t` is more than 300 seconds from now. This stops someone replaying an old delivery.
4. Compute the HMAC of `{t}.{raw body}` with your secret and compare it to `v1` with a constant-time comparison.

```js title="verify.js"
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyOatmilkSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map(part => part.trim().split("=")));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}

// Use the raw request body exactly as received, before JSON parsing:
// verifyOatmilkSignature(body, request.headers.get("Oatmilk-Signature"), process.env.OATMILK_WEBHOOK_SECRET)
```

```python title="verify.py"
import hashlib
import hmac
import time


def verify_oatmilk_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance_seconds:
        return False
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

```ts title="app/webhooks/oatmilk/route.ts"
import { verifyOatmilkSignature } from "@/lib/oatmilk-signature";

export async function POST(request: Request) {
  const rawBody = await request.text();
  const signature = request.headers.get("Oatmilk-Signature") ?? "";
  if (!verifyOatmilkSignature(rawBody, signature, process.env.OATMILK_WEBHOOK_SECRET!)) {
    return new Response("Invalid signature", { status: 400 });
  }
  const event = JSON.parse(rawBody);
  if (await alreadyHandled(event.id)) return new Response(null, { status: 200 });
  await enqueue(event);
  return new Response(null, { status: 200 });
}
```

## Try it here

Paste a delivery's raw body, its `Oatmilk-Signature` header and your endpoint's secret to see whether it verifies, and why not if it doesn't. You can also sign a body with your own secret to send a test delivery to your server. Everything runs in your browser: nothing you type here is sent anywhere.

In the HTML version of this page, a playground verifies a delivery or signs a test body with your own secret, entirely in the browser.

## When verification fails

| Symptom | Likely cause |
| --- | --- |
| Every delivery fails | The wrong secret, or a framework parsed the body before you read it. |
| Deliveries fail after you rotated the secret | Your server still uses the old secret. Rotation takes effect for the next delivery. |
| Only some deliveries fail | A proxy or middleware is changing the body, such as re-encoding characters. |
| Failures mention the time | Your server's clock is off by more than 300 seconds. Sync it with NTP. |

## Rotate a secret

Rotate an endpoint's secret in Developers › Webhooks or with `webhooks.endpoints.rotateSecret`. The new secret is returned once and signs every delivery from then on, so update your server first, then rotate.
