# Verify signatures

HMAC-SHA256 signing scheme — verify before you trust an event.

# Verify signatures

Every webhook is signed with your endpoint's `whsec_…` secret. Verify the signature **before** parsing the body.

## Header

```
Southbill-Signature: t=1735689600,v1=6a76…f3
```

- `t` — Unix timestamp of when Southbill generated the signature.
- `v1` — HMAC-SHA256 of `"{t}.{raw_body}"` using your `whsec_…` secret, hex-encoded.

## Steps

1. Extract `t` and `v1` from the header.
2. Reject if `|now - t| > 300` seconds (replay protection).
3. Compute `expected = HMAC_SHA256(secret, t + "." + rawBody)`.
4. Compare `expected` with `v1` in constant time.

## Node.js example

```js
import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now()/1000 - t) > 300) throw new Error("expired");
  const expected = crypto.createHmac("sha256", secret)
    .update(`${t}.${rawBody}`).digest("hex");
  const ok = crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(parts.v1, "hex")
  );
  if (!ok) throw new Error("bad_signature");
}
```

## PHP example

```php
[$t, $v1] = [null, null];
foreach (explode(',', $_SERVER['HTTP_SOUTHBILL_SIGNATURE']) as $p) {
  [$k, $v] = explode('=', $p, 2);
  if ($k === 't')  $t  = (int)$v;
  if ($k === 'v1') $v1 = $v;
}
if (abs(time() - $t) > 300) http_response_code(400);
$expected = hash_hmac('sha256', $t.'.'.$raw, $secret);
if (!hash_equals($expected, $v1)) http_response_code(400);
```

**Always use the raw request body** — parsing to JSON first will change whitespace and invalidate the signature.

