API implementation

Verify webhook signatures before parsing the body

Verify a webhook against the exact bytes received before JSON parsing changes their representation. Eight executed fixture cases show that valid JSON can fail signature verification, while a correctly signed replay can still pass it. Integrity, freshness and duplicate-effect control need separate checks.

Executed cases
8 envelope cases: 2 accepted, 6 rejected
Replay result
Two accepted deliveries, one effect in a local deduplication set
Boundary
Educational HMAC envelope; no provider SDK, network or durable store tested
A sealed message crosses a verification gate before separating into structured data.
Conceptual illustration of verifying received bytes before interpreting message fields.

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.

Illustrative procedure
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.

Bounded received bytes and signature headers enter verification before JSON parsing and business processing.
Figure 1. Proposed handler order: enforce ingress limits, preserve raw bytes, verify the provider envelope, then parse and process the event. View full-size figure.

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.

Outcomes for the eight educational envelope fixtures
Fixture caseAuthentication and freshness resultWhat it isolates
Original bytesAcceptedValid message under the assigned policy
Equivalent JSON, different bytesRejectedSerialization changes signed input
Signature from another keyRejectedEndpoint key mismatch
Modified valueRejectedBody integrity
Authentic message aged 301 secondsRejectedMaximum accepted age is 300 seconds
Authentic timestamp 31 seconds aheadRejectedFuture allowance is 30 seconds
Malformed signatureRejectedInput grammar
Immediate replay of original deliveryAcceptedFresh 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.

The original body and its immediate replay both authenticate; a local event set allows only one effect.
Figure 2. Executed fixture: original and immediate replay both pass verification. The separate local event set permits one effect across the two accepted deliveries. View full-size figure.

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 .

  1. Stripe: webhook signatures and replay protection
  2. Stripe: resolve signature verification errors
  3. Python: hmac and compare_digest

Continue the conversation

Comments (10)

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. Dreamtsoft Editorial

    Malformed signature input needs its own rejection case. It checks the envelope grammar before the endpoint can make an authentication decision.

Leave a comment

Your name and comment stay in this page and are cleared after the spam check.

10–2,000 characters. Keep the discussion relevant to this article.

Spam protection verification
Spam protection loads when you begin the form.

JavaScript is required to use this form and its spam protection.