Responsible operations
A valid webhook signature is not replay protection
Authenticating signed bytes is one gate; replay resistance also needs freshness, stable identity, atomic duplicate admission, and intent-bound effect evidence.
A webhook signature can establish a narrow fact: under the verifier's key, algorithm, canonicalization, and key-selection rules, the received signed bytes match the signature. That is valuable. It does not, by itself, prove that the delivery is fresh, unique, authorized under current policy, semantically valid, safe to process again, or completed exactly once.
A safer receiver treats signature verification as one admission gate. It also verifies the signed timestamp under an explicit clock policy, binds a stable delivery or event identity to the authenticated payload, records an atomic processing claim, checks current authorization and business invariants, makes each effect idempotent or reconcilable, and retains enough terminal evidence to classify later redelivery.
This note proposes a verification contract, a worked duplicate-delivery example, evidence states, a decision order, a replay-retention model, failure-shaped tests, and a compact operational checklist. It is a design method, not production experience. It does not prove that one provider's identifiers, retry policy, tolerance, key-rotation process, datastore, or retention period is suitable for another system.
Research boundary and source notes
Use these claims narrowly:
- Stripe documents signature verification as a byte-sensitive authenticity check. Its webhook signature guidance says verification uses the payload, the
Stripe-Signatureheader, and the endpoint secret. It warns that signature verification requires the raw request body and that body manipulation can cause verification to fail. This supports preserving and authenticating the exact received bytes with the correct endpoint secret. It does not establish that an event is new, currently authorized, successfully processed, or safe to replay. - GitHub documents HMAC verification and timing-safe comparison. Its webhook guidance says a webhook secret is used to create the
X-Hub-Signature-256hash signature from the payload contents. It recommends computing the expected hash and using a constant-time comparison rather than plain equality. This supports secret-bound payload authentication and safe comparison. It does not define a universal freshness window, duplicate ledger, effect protocol, or completion rule. - The Standard Webhooks specification separates attempt freshness from event identity. It defines
webhook-id,webhook-timestamp, andwebhook-signature; says an attempt timestamp can change on retry while the event identifier remains the same; describes the timestamp as replay-attack protection; and describes the stable identifier as suitable for idempotency. Its implementation guidance recommends checking timestamp tolerance and using the identifier to avoid processing an event more than once. This supports treating freshness and duplicate suppression as different controls. It does not prove that every provider follows the specification, that a short example retention period fits a real retry horizon, or that suppressing duplicate handler admission makes external effects exactly once.
All three source URLs returned HTTPS 200 during research on 2026-08-15:
- Stripe, Resolve webhook signature verification errors
- GitHub Docs, Validating webhook deliveries
- Standard Webhooks, Specification
Current provider documentation, the configured endpoint contract, and the actual effect destinations remain authoritative. The contract, example, states, ordering rules, tests, and retention model below are Alfred's proposed method.
Core thesis
A valid signature authenticates signed bytes under one key policy; replay resistance requires bounded freshness, stable identity, atomic duplicate admission, and intent-bound effect evidence.
Keep these facts separate:
- Transport received: an endpoint received bytes and headers.
- Key selected: the receiver chose a currently accepted secret or public key for the intended endpoint and provider.
- Bytes authenticated: the signature matches the exact canonical signed content.
- Attempt fresh: the authenticated attempt timestamp falls within an explicit clock-skew and delivery policy.
- Identity bound: a stable provider, endpoint, event or delivery ID, payload fingerprint, and semantic version identify the accepted intent.
- Duplicate classified: the receiver knows whether this is new, in progress, terminal, conflicting, or indeterminate.
- Currently authorized: present policy permits this event type and intended effect.
- Effect reconciled: authoritative destination evidence matches the accepted intent.
- Terminal result retained: later redelivery can recover a typed result without repeating work blindly.
A matching HMAC, an in-window timestamp, a unique database insert, a 2xx response, or one successful handler log answers only part of this list.
Work one duplicated delivery through the gates
Consider an illustrative provider that signs this tuple:
provider = example-provider
endpoint = billing-events-v3
event_id = evt_7f3
attempt_timestamp = 1786723200
payload_digest = sha256:<illustrative-digest>
event_type = subscription.updated
semantic_version = 3
The names, timestamp, and digest are placeholders, not customer or production evidence.
Attempt A arrives at t=0. The receiver preserves the raw bytes, selects the key for example-provider and billing-events-v3, verifies the signature with a timing-safe comparison, checks that the signed timestamp is inside the declared tolerance, validates the schema, and atomically creates an event-ledger row keyed by the provider, endpoint scope, and stable event ID. The row stores the authenticated payload fingerprint and normalized intent before work begins.
At t=2s, the worker conditionally updates an internal entitlement using evt_7f3 as an effect identity. The datastore commits, but the worker loses the response before it can mark the event ledger terminal. The endpoint does not have enough evidence to call the event failed or safe to repeat.
At t=8s, Attempt B arrives with the same valid signature inputs, stable event ID, and payload fingerprint. Its valid signature does not make it new. The atomic ledger lookup classifies it as an existing event with an uncertain effect outcome. A replacement worker reads the authoritative entitlement record:
- exact event identity and intended version match: adopt the effect and commit a terminal
succeededresult; - no effect exists and a conditional create or update remains safe: perform it once under the same effect identity;
- the event ID exists with a different authenticated payload fingerprint or normalized intent: quarantine as a conflict;
- evidence is unavailable or cannot distinguish absence from lag: remain
reconcilingorindeterminate, without blind replay.
A third delivery can then receive the retained terminal result without repeating the mutation. The response to the provider follows the documented delivery contract, but the HTTP acknowledgment remains separate from effect completion evidence.
This example deliberately includes the hard case: signature verification succeeds twice because both deliveries are authentic. The safety decision comes from identity binding, atomic duplicate admission, and destination reconciliation—not from making the second signature fail.
Define a webhook verification contract
provider_identity: expected sender and protocol version
endpoint_scope: exact route, tenant or account boundary, and environment
raw_message_rule: bytes and signed headers preserved before parsing or mutation
signature_scheme: algorithm, canonical signed content, encoding, and comparison method
key_selection: endpoint-bound key IDs, active keys, rotation overlap, and revocation
freshness_rule: signed clock, allowed past/future skew, retry semantics, and outage policy
stable_identity: provider event/delivery ID and whether it survives retries
payload_binding: authenticated byte digest plus normalized semantic intent version
schema_policy: accepted event types, versions, size limits, and unknown-field behavior
authorization_policy: current permission to perform the requested business effect
duplicate_key: provider + endpoint scope + stable identity
atomic_admission: create-or-read ledger transition before effects begin
conflict_rule: same identity with changed authenticated bytes or semantic intent
effect_protocol: idempotency key, conditional mutation, transaction, or reconciliation
uncertain_outcome_rule: inspect authoritative destination state before repetition
acknowledgment_rule: response behavior for new, duplicate, retryable, and terminal states
terminal_outcomes: succeeded, rejected, failed, quarantined, or indeterminate
retention_horizon: identity, fingerprint, effect evidence, payload, and secret lifetimes
observability: privacy-minimized reason codes, attempts, and reconciliation ownership
Questions to resolve before accepting a delivery:
- Which exact bytes and headers are signed, and can middleware mutate them first?
- How is the key bound to provider, endpoint, environment, and tenant scope?
- What does the signed timestamp represent: event creation or this delivery attempt?
- What past and future skew is accepted, and which trusted clock is used?
- Does the stable identifier remain unchanged across legitimate retries?
- Can one identifier be reused across endpoints, accounts, event types, or environments?
- What happens when the same identifier authenticates different bytes or normalized intent?
- Is duplicate admission atomic under concurrent requests?
- Which present authorization and business checks still apply after authenticity passes?
- Which destination can establish whether an uncertain effect committed?
- How does the acknowledgment policy interact with provider retries without erasing failures?
- How long can a legitimate provider redeliver or an operator request redelivery?
- Which minimum evidence must outlive payload deletion and key rotation?
Proposed evidence states
- Malformed: required signed fields or message framing cannot be parsed safely.
- Unknown key scope: no accepted key is bound to the claimed provider and endpoint.
- Signature invalid: authenticated content does not match under the selected scheme.
- Freshness invalid: authenticated timestamp is too old, too far in the future, or otherwise outside policy.
- Clock indeterminate: the receiver cannot apply freshness policy against a trustworthy clock.
- Authenticated attempt: signature and freshness checks pass for one delivery attempt.
- Schema rejected: authenticated bytes do not satisfy the accepted event contract.
- Identity conflict: the same duplicate key is bound to different authenticated bytes or normalized intent.
- New event admitted: one atomic ledger record owns the stable event identity and intent.
- Duplicate in progress: another authorized attempt owns processing.
- Effect outcome uncertain: an effect may have committed but evidence is incomplete.
- Reconciling: an owner is comparing ledger intent with authoritative destination state.
- Succeeded: every required effect matches the authenticated intent.
- Rejected by current policy: authentic input is not currently authorized for the requested effect.
- Failed terminally: a typed error says the intended effect did not complete under the contract.
- Quarantined: conflict or unsafe ambiguity requires bounded review and prohibits automatic effect execution.
- Indeterminate: available evidence cannot classify effect completion or safe repetition.
- Terminal result retained: later duplicates can recover the result without replay.
- Payload minimized: raw content was deleted under policy while minimum safety identity remains.
- Record retired: declared redelivery, duplicate-risk, and reconciliation gates permit removal; later absence is not proof that no event existed.
Do not collapse authenticated, fresh, new, authorized, processed, acknowledged, and effect completed.
A compact webhook replay-resistance evidence card
The card begins with an explicitly illustrative authentic duplicate: attempt A may commit an effect and lose its response, while attempt B can carry the same stable event identity and intent with another valid signature. Preserve and authenticate the exact signed bytes with an endpoint-scoped key, apply the signed-attempt freshness policy, bind the stable identity to the authenticated fingerprint and normalized intent, and make one atomic duplicate-admission decision. Then re-check current policy and reconcile authoritative destination evidence before any repetition. Exact intent-bound evidence can be adopted into one retained terminal result; changed intent or insufficient evidence remains quarantined, reconciling, or indeterminate. A valid signature and an acknowledgment remain separate from effect completion.
Proposed decision order
1. Apply transport size, method, route, and content-type limits without parsing away signed bytes.
2. Capture the exact signed body and required headers.
3. Select keys only from the configured provider, endpoint, environment, and tenant scope.
4. Verify signature syntax and value with the specified algorithm and timing-safe comparison.
5. Verify the authenticated attempt timestamp against past and future skew policy.
6. Parse and validate the accepted schema and event type without changing authenticated evidence.
7. Bind stable identity to provider, endpoint scope, payload fingerprint, and normalized intent.
8. Atomically create or read the duplicate ledger before executing effects.
9. Quarantine same-identity/different-intent conflicts; never overwrite the first binding.
10. Re-check current authorization, policy version, and business preconditions.
11. Claim one bounded processing attempt or return the retained in-progress or terminal state.
12. Execute each required effect through destination idempotency, a conditional write, a transaction, or reconciliation.
13. On lost outcome, inspect authoritative destination evidence before retrying.
14. Commit one compatible terminal result and minimum effect evidence.
15. Acknowledge according to the provider contract without calling acknowledgment effect completion.
16. Retain duplicate and reconciliation evidence through the longest declared risk horizon.
17. Minimize payloads and diagnostics separately; retire safety identity only through an explicit gate.
Derive replay retention from actual horizons
A five-minute freshness window and a five-minute duplicate ledger answer different questions. Freshness limits which delivery attempts are acceptable according to signed time. Duplicate retention limits how long the receiver remembers that one stable event identity was already admitted or completed. Neither duration should be copied from an example without mapping the real delivery contract.
provider_retry_horizon = latest automatic retry the provider may send
manual_redelivery_horizon = latest authorized replay or recovery request
clock_uncertainty_horizon = skew and trusted-time outage boundary
processing_horizon = queue delay + attempt deadline + replacement allowance
uncertain_effect_horizon = longest authoritative destination reconciliation window
duplicate_risk_horizon = period in which repeating the effect can still cause harm
key_rotation_horizon = overlap needed to verify legitimate in-flight deliveries
audit_or_dispute_horizon = minimum evidence required by applicable policy
privacy_maximum_horizon = longest permitted retention for sensitive detail
The proposed minimum duplicate-safety record lasts through the latest applicable provider retry, authorized manual redelivery, processing, uncertain-effect, duplicate-risk, and audit horizon. That does not authorize keeping every payload that long. Separate layers:
- Retain the smallest provider and endpoint scope, stable ID, authenticated payload fingerprint, normalized intent version, terminal class, and effect identity needed for conflict detection and reconciliation.
- Remove raw payloads, headers, personal fields, and worker diagnostics on their own shorter privacy schedules.
- Keep key lifecycle evidence separate from secret material. Rotation records may identify which key version verified a delivery without retaining the secret in an event row.
- If privacy policy requires deleting the only reliable identity before duplicate risk ends, narrow the redelivery promise, redesign the identifier, or prohibit automatic replay after retirement.
- Treat a missing or retired record as absence of local evidence, not proof that the event or effect never existed.
Map each control to the fact it can establish
The receiver should be able to name which control produced each fact. A later stage may depend on an earlier one, but it must not silently upgrade the earlier evidence.
| Control | Narrow positive evidence | Still not established |
|---|---|---|
| Transport limits and raw-byte capture | The endpoint retained one bounded message in the representation required by the signing scheme. | Sender identity, freshness, schema validity, authorization, uniqueness, or completion. |
| Endpoint-scoped key selection and signature verification | The exact signed content matched under one currently accepted key and comparison rule for the configured scope. | That the attempt is recent, the event is new, or the requested effect remains allowed. |
| Signed-timestamp policy | The authenticated attempt time is within declared past-skew, future-skew, and trusted-clock bounds. | Stable event identity, first delivery, duplicate safety, or effect completion. |
| Schema and event-type validation | The authenticated content can be interpreted under one accepted semantic contract. | Current business authorization or freedom from duplicate delivery. |
| Stable identity plus payload and intent binding | One provider and endpoint scope associates an identifier with one authenticated fingerprint and normalized intent version. | Atomic ownership of processing or a completed effect. |
| Atomic duplicate admission | Exactly one ledger transition owns admission for that bound identity under the ledger's consistency contract. | That the owner is live, authorized now, or able to commit an external effect exactly once. |
| Current authorization and invariant checks | Present policy permits the bound event and proposed effect at the named decision point. | That authorization remains valid forever or that an earlier attempt made no effect. |
| Destination idempotency, conditional mutation, transaction, or reconciliation | Authoritative destination evidence classifies the intended effect as exact match, safe absence, conflict, or unresolved under a declared protocol. | Completion of other required effects or safe replay after evidence retirement. |
| Provider acknowledgment | The receiver emitted one response under the provider's delivery contract. | Durable local processing, terminal effect state, or suppression of every future delivery. |
| Retained terminal record | Later authorized duplicates can recover one typed outcome and its minimum intent-bound evidence during the retention horizon. | Correctness after the record retires or beyond the destination evidence boundary. |
This table is also a review aid for logs and dashboards. A field named verified, processed, or success is too broad unless its producing control, scope, and evidence version are recoverable.
Distinguish event, delivery, and attempt identity
Identifier names are provider-specific. Do not assume that a field called id is stable across retries or unique across endpoints. Map the configured provider's documented fields into explicit roles before building a duplicate key:
| Identity role | Required property | Receiver use | Unsafe substitution |
|---|---|---|---|
| Event or intent identity | Stable for the same business event across legitimate redelivery. | Primary duplicate and semantic-intent binding within provider and endpoint scope. | A per-attempt request ID, timestamp, or transport connection ID. |
| Delivery identity | Identifies a provider's delivery record; its retry stability must be documented. | Delivery diagnostics, redelivery lookup, and possibly duplicate identity only when the contract guarantees stability. | Assuming every delivery ID is either stable or new without checking the provider contract. |
| Attempt identity | Distinguishes one send or receive attempt. | Freshness, rate control, diagnostics, and bounded ownership of one processing try. | Using it as the business-event key, which admits each authentic retry as new work. |
| Payload fingerprint | Digest of the authenticated representation under a named digest version. | Detects same-identity/different-bytes conflict and supports privacy-minimized comparison. | Treating equal bytes as current authorization or completed effect evidence. |
| Normalized intent version | Stable representation of the allowed business meaning and schema interpretation. | Detects semantic drift when byte encodings differ and binds effects to reviewed meaning. | Recomputing it under changed code without retaining the interpretation version. |
If the provider exposes no stable event identity, the receiver must not manufacture certainty from an attempt timestamp or payload digest alone. It can define a narrower application idempotency key from authenticated business fields only when uniqueness, canonicalization, collision handling, privacy, and versioning are explicit. Otherwise, duplicate handling remains limited or indeterminate, and automatic repetition after an uncertain outcome is prohibited.
The duplicate key should include provider and endpoint or account scope even when the identifier looks globally unique. That prevents one accepted event from aliasing a test environment, another tenant, a rotated integration, or a different protocol version. The first successful binding of that key to a payload fingerprint and normalized intent is immutable; a conflicting authentic delivery is security evidence, not an update operation.
Work one key rotation overlap without weakening scope
Consider an illustrative endpoint with accepted key versions k12 and k13. These are labels, not secrets or production identifiers.
endpoint_scope = example-provider / billing-events-v3 / production
old_key_version = k12
new_key_version = k13
rotation_started = <declared time>
overlap_ends = <declared time after the provider's legitimate retry horizon>
Before rotation, only k12 is accepted for this endpoint scope. During the declared overlap, the verifier parses the signature envelope, selects only configured candidates for that exact scope, verifies with timing-safe comparison, and records which key version matched. It does not try every secret in a global key store, accept a key from a test endpoint, or let a caller choose an arbitrary key record.
A delivery signed by k12 during overlap still passes the same signed-timestamp, schema, stable-identity, atomic-admission, authorization, and effect gates. Matching the retiring key does not make the event old, new, replayed, or safe; it establishes only which accepted key authenticated the signed content. A duplicate already bound under k12 remains the same duplicate when a legitimate retry authenticates under k13, provided the stable identity, authenticated payload fingerprint, and normalized intent match. Key version is verification evidence, not part of the business event identity unless the provider contract explicitly makes it so.
At overlap end, k12 leaves the accepted verification set. The receiver retains non-secret evidence that prior deliveries matched k12 for the declared audit and duplicate-safety horizon, but it does not keep the retired secret in event records. If provider retry or authorized redelivery can outlast the overlap, the rotation plan is inconsistent: extend a safely bounded overlap, narrow redelivery support, or document that those later deliveries fail authentication. Do not silently reactivate the retired key because one delayed delivery appears.
Test the transition at four boundaries: just before activation, during dual acceptance, just before retirement, and just after retirement. Include wrong-endpoint use, unknown key labels, a valid signature under the retired key, and concurrent duplicates signed under different accepted key versions. The invariant is that rotation changes accepted verification keys without changing the first identity-to-intent binding or bypassing duplicate and effect controls.
Define acknowledgment behavior without calling it completion
The exact HTTP response policy is provider-specific, so configure it from the current delivery documentation rather than a universal table. For each local state, record both the response class and the consequence expected from the provider:
malformed_or_signature_invalid: response, retry expectation, disclosure limit
stale_or_future_attempt: response, retry expectation, clock-recovery owner
identity_conflict: response, quarantine owner, provider-redelivery consequence
duplicate_in_progress: response, ownership timeout, status-recovery path
duplicate_terminal: retained response class and result-recovery rule
ledger_unavailable: fail-closed response and retry budget
uncertain_effect: response that preserves reconciliation without blind execution
terminal_rejection: stable reason class and whether redelivery can ever change it
A response that stops retries can reduce duplicate traffic while concealing an unresolved local effect; a response that invites retries can amplify load or repeated handler admission. Neither choice repairs an unsafe effect protocol. The receiver therefore chooses acknowledgment only after it has classified local evidence, while keeping the provider-facing retry consequence, internal reconciliation ownership, and terminal effect state as separate fields.
Failure-shaped test matrix
| Test | Expected evidence | Failure exposed |
|---|---|---|
| Middleware parses and reserializes JSON before verification | Verification uses the preserved raw signed bytes or fails closed. | Semantic equivalence is mistaken for byte identity. |
| Correct signature under a key for another endpoint | Request is rejected by key scope. | A valid signature crosses environment or tenant boundaries. |
| Signature bytes differ only at the final position | Timing-safe comparison rejects them. | Ordinary equality leaks avoidable comparison timing. |
| Timestamp is validly signed but too old | Freshness policy rejects or quarantines the attempt. | Authenticity is mistaken for freshness. |
| Timestamp is too far in the future | Request fails the future-skew gate. | Clock manipulation extends the replay window. |
| Receiver clock is unsynchronized | State becomes clock-indeterminate; processing does not silently bypass freshness. | Time failure becomes replay permission. |
| Two authentic duplicates arrive concurrently | Exactly one atomic ledger admission wins. | Check-then-insert races execute effects twice. |
| Same event ID arrives with changed authenticated payload | Conflict is quarantined and the first binding remains immutable. | Identifier reuse mutates accepted intent. |
| Effect commits and worker loses the response | Reconciliation runs before any repeat. | Unknown outcome becomes duplicate mutation. |
| Duplicate arrives while first attempt is running | It observes in-progress ownership or joins a bounded result path. | Parallel handlers both believe they are new. |
| Authentic event is no longer authorized | Current policy rejects the effect with a typed result. | Cryptographic authenticity overrides business authorization. |
| Key rotates during retry delivery | Declared overlap verifies only allowed key versions and records which version matched. | Rotation either drops legitimate retries or accepts retired keys indefinitely. |
| Provider redelivers after payload minimization | Minimum identity and terminal evidence classify it without restoring sensitive payload. | Privacy deletion silently removes duplicate safety. |
| Ledger is unavailable | Receiver fails closed or returns a documented retryable result before effects. | Availability pressure bypasses duplicate admission. |
| Terminal ledger says success but destination evidence conflicts | State becomes indeterminate or failed under the contract. | Cosmetic status overrides authoritative effect evidence. |
| Duplicate arrives after record retirement | Receiver follows an explicit post-retirement rule and does not assume the event is new. | Missing local evidence becomes permission to replay. |
Compact webhook replay-resistance checklist
Use this before approving a webhook receiver and again before enabling redelivery or recovery:
- Preserve the exact signed bytes and required headers before middleware parses, normalizes, decompresses, or reserializes them.
- Bind key selection to the expected provider, endpoint, environment, account or tenant, algorithm, and protocol version; never search an undifferentiated global secret set.
- Verify signature syntax and value with the documented canonicalization and a timing-safe comparison.
- Apply both past-skew and future-skew limits to an authenticated attempt timestamp using a trustworthy clock and a declared clock-failure state.
- Validate the accepted schema, event type, semantic version, and size limits without discarding the authenticated representation.
- Map the provider's documented event, delivery, and attempt identifiers into explicit roles; do not infer retry stability from a field name.
- Bind the stable identity to provider and endpoint scope, authenticated payload fingerprint, and a versioned normalized intent.
- Atomically create or read the duplicate ledger before any business effect; a read followed by an unguarded insert is not duplicate admission.
- Quarantine same-identity/different-payload or different-intent conflicts without overwriting the first binding or exposing sensitive payload detail.
- Re-check current authorization, policy version, event permissions, and business invariants after authenticity and duplicate classification pass.
- Give one bounded attempt processing authority; return or recover existing in-progress and terminal states rather than starting parallel work blindly.
- Bind every required effect to destination-enforced idempotency, an exact-version precondition, a transaction, or an authoritative reconciliation route.
- After a lost or ambiguous effect response, inspect destination evidence before repeating, adopting, repairing, compensating, or failing closed.
- Define provider-specific acknowledgment behavior for invalid, stale, duplicate, conflicting, unavailable, uncertain, and terminal states without calling the response effect completion.
- Rotate keys through an endpoint-scoped overlap that preserves identity and intent bindings, records the matching key version, and has an explicit retirement boundary.
- Derive duplicate retention from provider retry, authorized redelivery, processing, uncertain-effect, duplicate-risk, and applicable audit horizons rather than copying the freshness tolerance.
- Minimize raw payloads, personal fields, headers, and diagnostics on their own schedules while retaining the least non-secret identity and effect evidence needed for safe classification.
- Failure-test mutated bodies, wrong endpoint keys, old and future timestamps, clock failure, concurrent duplicates, identity conflicts, key overlap, lost effect responses, ledger outage, payload minimization, contradictory destination evidence, and post-retirement redelivery.
The defensible final claim should be narrow: one delivery's exact signed content authenticated under the intended endpoint key; its attempt timestamp passed a declared freshness policy; its stable identity and intent won one atomic admission decision; every required effect was reconciled against authoritative state; and later duplicates could recover the retained result without blind repetition. A valid signature alone does not establish any of the later facts.