Keep the signed bytes intact
Verify a webhook against the bytes the sender signed before converting its payload into application objects. Two JSON documents can describe the same event while producing different message authentication codes. Reformatting whitespace is enough to break a signature over the original body.
Start with the runnable HMAC fixture. It uses a public test key, an assigned clock and a deliberately small envelope. Eight checks distinguish the original delivery, altered bytes, the wrong key, stale or future timestamps and a malformed signature. The original delivery and its immediate replay both pass authentication. Six other cases fail. A separate local deduplication example applies the event only once.
Those outcomes establish a byte and freshness boundary for the fixture. They do not validate a deployed endpoint, replace a provider SDK or prove that a distributed consumer applies side effects once. Use an actual provider's supported verifier when implementing its webhook protocol.
Build the message that the sender authenticated
The example signs an ASCII timestamp, a period and the original body bytes. Its body is {"id":"event-1", "value":7}. Parsing and serializing it without the space yields an equivalent JSON object, but a different byte sequence.
message = str(timestamp).encode("ascii") + b"." + raw_body
expected = hmac.new(test_key, message, hashlib.sha256).hexdigest()Stripe's signature documentation describes this timestamp-plus-body construction for its signing scheme. Its full header can contain several signatures and version fields. Our fixture passes an already-separated timestamp and one hexadecimal signature to a small function. It deliberately does not implement that header grammar.
Preserve the original body at the endpoint boundary. Framework middleware, an integration gateway or a logging wrapper can parse and rebuild it before the verifier runs. Stripe's troubleshooting guide identifies body changes and incorrect endpoint secrets as causes of verification failure. Inspect configuration and the body-handling path without printing a live signing secret.
Keep verification ahead of business decisions based on parsed fields. A payload's claimed customer identifier is untrusted until the envelope passes the configured verification policy, and it still needs mapping to an authorized application account afterward. Authenticity identifies a source with the signing key; it does not grant arbitrary tenant access.
Compare authentication and freshness separately
The fixture rejects non-integer timestamps, bodies larger than 4096 bytes and signatures that are not exactly 64 lowercase hexadecimal characters. Those are chosen input constraints. A real endpoint must enforce its body limit while receiving the request, before unbounded allocation, rather than treating this buffered-function check as transport protection.
Use a constant-time comparison primitive for the computed and supplied authentication codes. The fixture calls Python's hmac.compare_digest; Python documents this primitive for avoiding content-dependent short-circuit comparisons. This article did not measure timing behavior or audit an interpreter implementation.
After authentication, apply the timestamp policy. The fixture accepts ages up to 300 seconds and timestamps up to 30 seconds ahead of its assigned clock. These limits make the examples reproducible. They are not the defaults of a complete Stripe SDK, and the test should not be read as an instruction to change a production verifier's tolerance.
An authentic old message remains authentic. The freshness check rejects it because its delivery falls outside the accepted interval. A forged timestamp does not make an old signature recent, since the timestamp participates in the authenticated message.
Run the negative cases without changing the policy
Download the script and run python3 experiment.py. It uses only the standard library and writes results.json beside itself. Every listed result is checked by an assertion.
| Fixture case | Authentication and freshness result | What it isolates |
|---|---|---|
| Original bytes | Accepted | Valid message under the assigned policy |
| Equivalent JSON, different bytes | Rejected | Serialization changes signed input |
| Signature from another key | Rejected | Endpoint key mismatch |
| Modified value | Rejected | Body integrity |
| Authentic message aged 301 seconds | Rejected | Maximum accepted age is 300 seconds |
| Authentic timestamp 31 seconds ahead | Rejected | Future allowance is 30 seconds |
| Malformed signature | Rejected | Input grammar |
| Immediate replay of original delivery | Accepted | Fresh authentication is not deduplication |
Do not relax the verifier until the broken example passes. If a body transformation caused the failure, fix where raw bytes are captured. If a different environment's key signed it, fix the environment mapping. Both interventions preserve the security boundary instead of widening it around a configuration error.
The local test key is intentionally public and unsuitable for a real endpoint. To extend the fixture, create additional synthetic cases or use test-mode events through a separately configured integration. Keep live payloads and credentials out of a public reproduction package.
An authentic retry can still repeat an effect
A replay inside the permitted interval passes the same checks as the first delivery. The script demonstrates this explicitly. It then uses a local set of event identifiers to produce one effect from two accepted deliveries. That set is an illustration of deduplication, not durable state and not safe across independent worker processes.
In an application, define the stable provider event identity and store its processing outcome with the business-state change when the storage model allows it. If an external side effect is involved, that external boundary needs its own retry contract. The webhook ordering and duplicate-delivery article covers the state transition after authentication.
A provider retry may also carry a new delivery timestamp and signature. Stripe documents that behavior, so saving only a previous signature as the duplicate key would confuse delivery identity with event identity. Check the provider's event identifier and retention requirements instead.
Verify the endpoint around the function
The final integration test should send provider-compatible test deliveries through the actual gateway and body middleware. Exercise the correct and incorrect endpoint secret, a mutated body and an authentic expired delivery. Confirm that a verified retry does not duplicate the intended effect after a process restart. None of those deployment checks is replaced by a passing local HMAC assertion.
Use the endpoint acceptance sheet to assign evidence for raw-body capture, clock handling, key selection and durable processing. The sheet remains a proposed checklist. Its last row asks for the observable outcome after a duplicate delivery crosses the real storage boundary.
Sources
Documentation checked .

Dreamtsoft Editorial
The equivalent-JSON case is a useful middleware test. A parser can preserve the data meaning while changing the byte sequence that the sender authenticated.
Dreamtsoft Editorial
Capture the signed input before a body transformation occurs. The endpoint review should identify where the original bytes remain available in the actual request path.
Dreamtsoft Editorial
A valid signature on an immediate replay exposes the boundary clearly. Authentication does not establish that the business effect has not already been applied.
Dreamtsoft Editorial
The freshness policy needs both an age limit and a decision about future timestamps. Record the clock assumptions before interpreting either rejection case.
Dreamtsoft Editorial
The fixture's timing allowances are assigned examples. A production endpoint needs the sender's contract and its own acceptance evidence before adopting those values.
Dreamtsoft Editorial
Wrong-key rejection belongs in the endpoint test alongside body mutation. An otherwise valid message sent to the wrong endpoint should not inherit another endpoint's trust.
Dreamtsoft Editorial
A process restart is a useful boundary for the duplicate-effect check. An in-memory record of earlier deliveries cannot demonstrate durable repeat protection.
Dreamtsoft Editorial
Keep the integration test focused on the same bytes that passed through the gateway. Testing only the verification function leaves body middleware behavior unresolved.
Dreamtsoft Editorial
The acceptance sheet should identify which storage operation makes a verified delivery's effect durable. Signature verification alone does not define that transaction boundary.
Dreamtsoft Editorial
Malformed signature input needs its own rejection case. It checks the envelope grammar before the endpoint can make an authentication decision.