# Webhooks: Signing and Verifying — API Design and Versioning

Source: https://www.geekswithgeeks.com/en/api-design/sec-webhooks

> Sign outgoing events and verify them on receipt, with a timestamp to stop replays.

## Anyone can POST to your URL

A **webhook** is an HTTP callback your API makes to a consumer's URL when an event occurs. Because the consumer's endpoint is public, it must be able to **verify the sender**. The standard approach: sign the request body with a shared secret using **HMAC-SHA256**, and send the signature (and a **timestamp**) in a header. The receiver recomputes the HMAC over the **raw bytes** of the body, compares with a **constant-time** function, and rejects timestamps older than a few minutes to prevent **replay attacks**. Webhook senders should also **retry** failed deliveries with backoff, include a unique **event ID** so receivers can de-duplicate, and let consumers rotate secrets. Receivers should respond `2xx` quickly and process the event asynchronously.

## Signing and verifying, run

I ran this plain-Python example. The signature is a 64-character hex digest. An untouched body within the time window verifies as `ok`; a body with one extra byte is rejected as a bad signature; the same valid message 4,000 seconds later is rejected as too old.

```python
import hmac, hashlib, json, time, bisect

SECRET = b"whsec_demo"
def sign(body: bytes, ts: int) -> str:
    return hmac.new(SECRET, f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
def verify(body, ts, sig, now, tolerance=300):
    if abs(now - ts) > tolerance: return "rejected: too old"
    return "ok" if hmac.compare_digest(sign(body, ts), sig) else "rejected: bad signature"
body = b'{"event":"payment.succeeded","id":"pay_1"}'; ts = 1_700_000_000
sig = sign(body, ts)
print(len(sig), verify(body, ts, sig, ts + 10), verify(body + b" ", ts, sig, ts + 10), verify(body, ts, sig, ts + 4000))

```

Output:

```
64 ok rejected: bad signature rejected: too old
```

## Verify the raw body

Re-serialising parsed JSON changes whitespace and key order and breaks the signature. Compute the HMAC over the exact bytes received.

**Quiz:** Why include a timestamp in the signed data?

- [x] To reject old, replayed messages
- [ ] To make the body smaller
- [ ] Because HMAC requires dates
- [ ] To speed up delivery

*Answer:* To reject old, replayed messages. Without freshness, an attacker could resend a captured valid message later.
