# Webhook verification: signed delivery is not new business work

Original LockedIn Labs workshop · 50 minutes · Python 3.11+ standard library. Ten offline cases verify actual HMAC-SHA256 values and execute scratch SQLite transactions. Each case generates fresh ephemeral keys; neither keys nor MAC values appear in reports. There is no HTTP endpoint, provider account, real event, persistent database or production authentication capability.

## 1. Give each boundary its own question

A **webhook** is a source sending a notification to a receiver, commonly through HTTP. Before acting, the receiver needs to know which configured source contract applies, whether the received bytes authenticate under that contract, whether their signed time is permitted, whether the payload is supported, and whether the business event was already applied. A successful connection answers none of those questions.

An **HMAC**, or keyed-hash message authentication code, binds bytes using a shared secret. Verification demonstrates agreement with the configured key and message bytes; it is not encryption, a public-key signature or proof of which individual held the shared key. A valid MAC does not grant end-user access or make the reported business change appropriate.

The source in this exercise is selected by receiver configuration, not by an unverified request field. It reports a fictional `synthetic.delta` of four units. The effect is a counter in one disposable memory database, not a payment, account update, clinical action or external command.

## 2. Verify the exact envelope before parsing JSON

This workshop uses an original teaching envelope. Its internal `lab-delivery-id`, `lab-sent-at` and `lab-mac` names are not a provider integration. The sender computes HMAC-SHA256 over:

```text
delivery_id ASCII bytes + "." + sent_at ASCII bytes + "." + exact body bytes
```

The raw body contains a separate stable `event_id`. The delivery ID identifies an attempt in this fixture; a later attempt can have another ID and send time while reporting the same business event. Real providers assign different semantics to their headers and IDs. Use their supported SDK and exact documented scheme; do not substitute these lab fields into a real endpoint.

The receiver bounds the body and header shapes, decodes one version-one Base64 MAC into exactly 32 bytes, and compares it with the full expected digest using `hmac.compare_digest`. It does not choose the comparison length from a supplied digest. It verifies the signed timestamp policy before parsing JSON or opening a receipt transaction.

A JSON parser can treat differently spaced documents as the same object. That does not make their bytes identical. Verifying a reserialized object instead of the original bytes changes the authenticated message. Keep raw bytes at the real HTTP/framework boundary according to the source contract; this offline program receives bytes directly and does not test middleware behavior.

## 3. Run ten independently reset receiving cases

Download [the Python runner](/templates/fde-webhook-receiving.py) into a disposable folder:

```sh
python3 --version
python3 fde-webhook-receiving.py > webhook-original.json
python3 fde-webhook-receiving.py --case valid --age-seconds 301 > webhook-aged.json
python3 fde-webhook-receiving.py --case repeat
python3 fde-webhook-receiving.py --case same-event-new-bytes
```

On Windows, use `py -3` instead of `python3`. There are no package installations or network requests. Each case creates and closes its own SQLite memory database. Shell redirection overwrites a file of the same name; retain original and changed reports separately. The CLI accepts no secret, URL, arbitrary body or external input file.

Predict the default outcomes:

| Case | Boundary actually tested | Actual local outcome |
| --- | --- | --- |
| `valid` | Original bytes, configured key and permitted send time | One receipt; four counter units |
| `body-changed` | Units changed to seven while retaining the old MAC | MAC mismatch; no parsing or receipt transaction |
| `body-reserialized` | Same JSON meaning, changed spacing, old MAC | MAC mismatch before receiver JSON parsing |
| `wrong-key` | Sender uses another ephemeral key | MAC mismatch; no local effect |
| `changed-delivery-id` | Delivery ID changed after signing | MAC mismatch because the ID is authenticated too |
| `stale` | Authentic bytes sent 301 logical seconds earlier | Timestamp refused; no receiver JSON parsing |
| `future` | Authentic bytes dated 31 logical seconds ahead | Timestamp refused under this fixture's policy |
| `repeat` | New delivery ID/time, same event and exact body, new valid MAC | First applied, second replayed; still four units |
| `event-conflict` | New valid delivery, same event ID, changed units | MAC passes, receipt conflicts; first effect retained |
| `same-event-new-bytes` | New valid delivery and same JSON meaning, different body bytes | MAC passes, strict byte-bound receipt conflicts |

Reports retain only synthetic delivery/time/body evidence, hashes, stage results, counters and database before/after state. MAC values and keys are omitted. The actual receiver increments `mac_comparisons`, `json_parses` and `receipt_transactions` when those stages execute; a rejected request does not become a recorded business receipt.

## 4. Treat freshness and idempotency as different controls

The receiver uses fixed fictional time `1800000000`, never the machine's wall clock. Its authored policy admits a signed age from minus 30 through plus 300 seconds. That is a teaching policy, not a universal webhook tolerance. Both stale and future cases have valid MACs and still fail this time check. A real owner must agree clock source, permitted skew, retry behavior and recovery evidence.

Now compare the default `valid` case with `--age-seconds 301`. Only that case's signed send time changes. Its MAC remains correct, but the receiver refuses it without parsing the payload or touching receipt state. The age control does not widen the receiver's policy. It helps distinguish a message-authentication result from a freshness decision.

Within the window, a delivery can still repeat. The `repeat` case independently signs delivery DEL-2 with a new send time while retaining event EVT-1 and its exact body. Both deliveries pass verification and payload checks. The receipt key is configured source plus business event ID, not MAC bytes, timestamp or delivery-attempt ID. An identical event/body replays the existing receipt without another counter increment.

A short timestamp window does not replace durable deduplication; an in-memory receipt does not establish durable recovery. If history expires or the process restarts, a real receiver needs an explicit retention/reconciliation contract. This exercise has no persistence, concurrent delivery or cross-system exactly-once guarantee.

## 5. Keep the receipt and local effect together

After verification, the receiver rejects duplicate JSON keys, unsupported fields/kinds, invalid event identity, Boolean/string/fractional units and out-of-contract amounts. A valid MAC authenticates supplied bytes; it does not make an unsupported payload acceptable.

For supported payloads, one actual SQLite transaction checks `(source, event_id)`, binds the receipt to the SHA-256 hash of the **exact body bytes**, inserts a new receipt and updates the local counter. A repeat with identical bytes replays. A different body under the same event key conflicts and preserves the first state. The runner includes a local failure hook for focused transaction tests; it is not reachable from the signed payload or CLI.

The strict byte binding is deliberate. `same-event-new-bytes` has a fresh valid MAC and the same parsed JSON meaning as the first delivery, yet its receipt conflicts because spacing changed. This is not a claim that whitespace changed the business meaning. It demonstrates that the receiving contract chose byte identity rather than semantic equivalence. Real systems may need a documented canonical representation or another independently validated business payload contract; do not silently invent one after a conflict.

A receiver must not acknowledge a database failure as a completed effect. Likewise, a committed receipt here establishes only a four-unit local counter change. It is not evidence of an external side effect, downstream acceptance, repair or a provider-specific acknowledgement/retry policy.

## 6. Receive verification evidence as a delivery pod

- **Builder:** retain exact runner/runtime versions and original/changed reports. Locate the authenticated fields, actual verification stage and committed receipt/effect evidence; keep keys and MAC values out of shared records.
- **Receiver:** reproduce a byte-change rejection, the aged valid-MAC refusal and a freshly signed repeat. Record your own action, observed stage counts and assistance rather than inheriting the builder's result.
- **Source, application and platform owners:** agree the real signature scheme/SDK, source-to-key mapping, clock policy, supported event meaning, receipt identity/retention and bounded recovery. Assign key provisioning/rotation, HTTP body preservation, durable isolation and downstream reconciliation to their actual owners.

Each participant keeps an individual record:

```text
Synthetic alias / role:
Exact runner / Python / SQLite / command:
Prediction before execution:
Observed delivery/time/body hash and evidence path:
MAC result / timestamp decision / receiver parse count:
Receipt identity / before and after local effect:
Changed send time / separate prediction and observation:
Own contribution / assistance:
Receiving action actually attempted / assistance:
Remaining boundary / next bounded test / accountable role:
```

The exercise does not implement key rotation, secret storage, TLS, HTTP parsing, provider compatibility, end-user authorization, durable replay history or real business permission. Practice evidence does not establish qualification, independence, placement or authority to deploy. Continue with [event and receiving semantics](/training/guides/integration-engineering) and [uncertain HTTP outcomes](/training/guides/http-outcomes) for separate ordering, acknowledgement and reconciliation questions.

## 7. Defend the contract in an interview

Explain why JSON reserialization can break verification while preserving parsed meaning, and why a freshly re-signed body can still conflict with an earlier receipt. Name the exact equality contract at each boundary: bytes, supported payload, event identity or local effect.

Explain why a valid MAC does not establish freshness, new work or end-user authorization. Use the repeat case to show why signature bytes and delivery-attempt IDs make poor business receipt keys in this specific contract. Identify what a shared-secret MAC cannot prove about a particular individual.

Finally, design the next bounded real-adapter test: preserve the actual provider's raw body and signed fields, verify its supported SDK/configuration, test a duplicate and unsupported event, and retain receipt/effect evidence under the real runtime's durability and concurrency policy. Source-specific identifiers, key rotation, clock skew and acknowledgement behavior must come from that integration's contract.

Sources checked 28 October 2026: [OWASP webhook security](https://cheatsheetseries.owasp.org/cheatsheets/Webhook_Security_Cheat_Sheet.html), [Python 3.11 HMAC and constant-time comparison](https://docs.python.org/3.11/library/hmac.html), [Stripe's endpoint/signature and duplicate-event guidance](https://docs.stripe.com/events/manage-webhook-endpoints), and [SQLite transaction behavior](https://www.sqlite.org/lang_transaction.html). Provider guidance explains real source boundaries; this original envelope does not implement the provider's scheme. The fixtures, comparisons, diagrams and receiving exercise are original.
