TechWeb Magazine illustration of two webhook deliveries passing through signature, freshness and duplicate checkpoints before one approved action
A webhook can be authentic and still arrive again. Verify it, reject stale replays, claim it once and make the final action safe to repeat.

A payment event reaches your endpoint. The signature is valid, so the handler credits an account. Seconds later, the same event arrives again and the credit runs twice.

Nothing forged the second request. A retry, redelivery or replay was enough. Signature checks answer who signed a message and whether covered data changed. They do not always prove that your system has never processed the event before.

Before a webhook can change money, access, inventory or customer records, require four gates. Check the signature, age, unique ID and a safe final action.

Keep the raw body until verification ends

Start with the provider's documented verification method. GitHub signs the payload body and sends the result in X-Hub-Signature-256. Its guidance says to compare the expected and received values with a constant-time method.

The byte sequence matters. A framework may parse JSON, alter white space or change encoding before your handler sees it. Stripe warns that those changes can break verification. Capture the raw request body first; parse it only after the signature passes.

Use the secret for that exact endpoint and environment. Stripe test and live endpoints have different secrets. Do not put a secret in the callback URL, source code or logs. The AI coding secrets guide shows where credentials often leak during development.

A valid signature can be replayed

An attacker may copy a valid signed request and send it again. A delivery system may also retry after a timeout. Both can present authentic bytes.

Freshness checks narrow that window when the provider signs a timestamp. Slack signs the request timestamp too. Its guide shows a five-minute example limit. Stripe also signs a timestamp, while its official libraries use a five-minute default tolerance.

Follow the provider's scheme, not a universal five-minute rule. GitHub's common webhook signature does not add a signed timestamp in the same way. GitHub instead tells receivers to track the unique X-GitHub-Delivery value to defend against replay.

Clock checks need a trustworthy clock. A server that drifts can reject fresh work or accept old requests. Keep time synchronized and alert on repeated failures near the boundary.

Claim the delivery before doing real work

After verification, read the provider's stable delivery or event identifier. Try to insert it into a store with a uniqueness rule. Only one insert may win.

A read followed by a later write is not enough. Two workers can both read “missing” and then run the action. Use a unique database key, compare-and-set operation or queue feature that makes the claim atomic.

GateQuestionFailure response
SignatureDid the expected provider sign these bytes?Reject and log a safe reason
FreshnessIs the signed timestamp inside the allowed window?Reject as stale
Unique claimHas this delivery or event ID won before?Acknowledge the duplicate without acting
Business actionCan the same command run twice safely?Return the stored result or make no change

Store enough state to explain the outcome. Keep the provider, endpoint, event type, delivery ID, first-seen time, processing state and final result. Avoid saving secrets or whole payloads unless retention is justified.

A queue protects the endpoint, not the business action

GitHub recommends a quick response and notes that a queue can move processing into the background. Stripe also tells endpoints to return a successful status before complex logic that may time out.

That pattern keeps the delivery path fast. It does not make the worker idempotent. A worker can crash after it changes a record but before it marks the job complete. The queue may then deliver the job again.

Carry a stable idempotency key into the operation itself. For an invoice event, that key might combine the provider, event ID and action name. Put a unique constraint beside the ledger entry or use the downstream API's documented idempotency feature.

Do not reuse one key for unrelated actions. “Send receipt” and “grant access” need separate records even when they start from the same event.

Design for retries without hiding failures

A duplicate that already succeeded should usually get a quick success response. A duplicate whose first attempt is still running may wait, return a safe success, or enter a review path. Choose one policy and record it.

Permanent errors need a dead-letter path. Bad event types, missing required fields and revoked accounts should not retry forever. Temporary database or network failures can retry with backoff.

Plan the recovery route before launch. The restore-test workflow helps verify stored webhook state, while the emergency admin drill covers access during an outage. Keep a named owner for failed deliveries. Test the replay path before it is needed.

Run a safe release check

  1. Use provider fixtures. Verify a known good payload and signature from official documentation or a provider test tool.
  2. Change one byte. Confirm the signature fails before any JSON parsing or side effect.
  3. Send the same delivery twice. Confirm only one worker wins the unique claim and the second request returns the planned result.
  4. Interrupt the worker. In a controlled non-production test, stop it between the business action and completion marker. Confirm a retry cannot repeat the change.
  5. Check logs. Keep delivery IDs and outcomes, but remove secrets and sensitive payload fields.

These are test cases to run, not claims that TechWeb tested your handler. Use a sandbox account and reversible data.

Frequently asked questions

Does HTTPS make a webhook authentic?

No. HTTPS protects a connection to your endpoint. Verify the provider's signature as well, using its documented scheme.

Is signature verification enough to stop duplicates?

No. A valid delivery can be retried or replayed. Track a stable delivery or event ID and make the final action idempotent.

Should every duplicate return an error?

Usually not. If the first attempt already succeeded, a quick success can stop more retries. Preserve the stored outcome for audits.

How long should delivery IDs be stored?

Cover the provider's retry and redelivery window plus your recovery needs. Longer retention can help audits, but it increases storage and privacy duties.

Sources

GitHub: Validating webhook deliveries · GitHub: Webhook best practices · Stripe: Receive webhook events · Stripe: Resolve signature verification errors · Slack: Verifying requests · IETF RFC 9421: HTTP Message Signatures. Provider formats and retry behavior can change; check the live documentation before deployment.

More in this sectionTechnology →