# Signatures & retries

Verify every payload, survive every retry.

## Signature header

```text
Southbill-Signature: t=1755423672,v1=6f1c…9ab2
```

Signed payload = `"{t}.{raw body}"`, HMAC-SHA256 with your endpoint secret (`whsec_…`), hex encoded.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(raw: string, header: string, secret: string, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!Number.isFinite(age) || age > toleranceSec) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${raw}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(String(parts.v1 ?? ""));
  return a.length === b.length && timingSafeEqual(a, b);
}
```

Verify against the **raw** body — parsing first and re-serializing breaks the signature.

## Rules for your endpoint

1. Answer `2xx` within **10 seconds**. Do the work asynchronously.
2. Be idempotent: store `event.id` and drop duplicates.
3. Do not rely on ordering. Compare timestamps or re-fetch the object.
4. Reject requests whose signature fails — never "process anyway and log".

## Retries

Failed or slow deliveries retry with exponential backoff for **24 hours**: roughly 1 min, 5 min, 30 min, 2 h, 6 h, 12 h. After that the delivery is marked failed and stays visible in the logs for manual replay.

An endpoint failing continuously for 72 hours is auto-disabled and you are notified.

## Secret rotation

Rotating issues a second active secret; both validate during a 24-hour overlap so you can deploy without dropped events. Old secrets are then destroyed. Secrets are stored encrypted (AES-GCM) and shown once.

## Replay

Any delivery can be replayed from the logs with the original payload — the retry counter and signature timestamp are refreshed.
