# The payment webhook keeps re-sending the same notification; how do I set up idempotency?

> Verify the HMAC constant-time over the raw body, return 200 fast and enqueue the work, and let a UNIQUE constraint on event_id enforce once-only.

- Asked: 2026-05-11
- Answered: 2026-05-14
- Asked by: Sıla
- Tags: mimari, guvenlik, api
- Source: https://muhammetsafak.com/just-ask/webhook-idempotency-and-hmac-for-duplicate-payment-notifications/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** I receive webhooks from a third-party payment provider. Because of network outages, the provider may send the same successful payment notification multiple times (retries).

How should I architect the webhook endpoint so my system doesn't double-process (double-spending)? How should request signing (HMAC) and DB-level idempotency key tracking work?


Short answer: these are **two separate concerns** — authenticity (HMAC) and preventing duplicates (idempotency). Don't conflate them; solve each separately.

## Short answer

In one sentence: the signature asks "is this really the provider", idempotency asks "have I done this work before". I wrote about the general form of the second one — the API-side rules for safely repeating the same request — in [idempotency in APIs](/blog/idempotency-in-apis-safely-repeating-the-same-request/); this is that idea applied to a webhook.

## Why

1. **Signatures and uniqueness guarantee different things.** A correctly signed notification can arrive five times; the signature tells you it's genuine, not which delivery it is.

2. **App logic can't give you a once-only guarantee.** If two requests arrive at the same time, "check then write" lets both through. Only the database's own constraint gives you uniqueness.

3. **A slow response manufactures more retries.** If the provider times out it resends the same notification; a synchronous handler enlarges its own problem.

## What to do

1. **Verify the signature over the RAW body.** Check the HMAC signature against the **raw body**, before parsing, with a **constant-time** compare. Decode and re-encode the JSON and you break the signature.

2. **Let the DB enforce idempotency.** Persist the provider's event id (or a hash) in a table with a **UNIQUE constraint**; do a **check-or-insert** inside a transaction.

3. **Return 200 fast first, enqueue the work.** Verify the signature, record it, return `200` fast, and push the real work to a queue; keep the worker idempotent on the same key. The operational detail of the queue side is in the [Laravel queue and Supervisor post](/blog/async-job-processing-with-laravel-queue-and-supervisor/).

4. **Be safe to call twice at every layer.** The handler, the worker, and the side effect must each produce one clean result when called twice with the same notification.

5. **Watch ordering and the replay window.** A "refund" notification can arrive before its "charge"; handle out-of-order cases.

**Bottom line:** I'd build the flow as **verify signature → return 200 fast → enqueue the work**, and make the DB enforce idempotency with a UNIQUE constraint on `event_id` + a transactional check-or-insert. Compare the HMAC constant-time over the raw body. Double-spending is stopped by the database's uniqueness guarantee, not by app code.

## Related Reading

- [Async Job Processing with Laravel Queue and Supervisor](/blog/async-job-processing-with-laravel-queue-and-supervisor/) — Blog
- [Should I move my API error bodies to the RFC 7807 problem+json format?](https://muhammetsafak.com/just-ask/move-api-error-bodies-rfc-7807-problem-json/) — Just Ask
- [My non-root container can't write to the mounted storage directory—how do I fix the permissions?](https://muhammetsafak.com/just-ask/my-non-root-container-cant-write-to-the-mounted-storage-directory/) — Just Ask
- [Should I author the OpenAPI spec first or generate it from my code?](https://muhammetsafak.com/just-ask/should-i-author-the-openapi-spec-first-or-generate-it-from/) — Just Ask
