# 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://www.muhammetsafak.com.tr/en/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.

In one sentence: the signature asks "is this really the provider", idempotency asks "have I done this work before". Build your architecture on that split:

1. **Verify the signature over the RAW body.** Check the provider's HMAC signature against the **raw body**, before parsing, with a **constant-time** compare; if it doesn't match, trust nothing and reject. Decode and re-encode the JSON and you break the signature — verify without touching the body.
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. The once-only guarantee comes from the database, not app logic — if the same id arrives a second time, the insert simply fails.
3. **Return 200 fast first, enqueue the work.** Don't run the payment side effects **synchronously** in the webhook handler; 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 response must be fast so the provider doesn't time out and retry.
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. Don't put a "it arrives once" assumption anywhere.
5. **Watch ordering and the replay window.** A "refund" notification can arrive before its "charge"; handle out-of-order cases. Know the provider's replay/retry window too, so you don't mistake an old notification for a new one and process it again.

**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. You'll find the operational detail of the queue side in the hub post.

## Related Reading

- [Async Job Processing with Laravel Queue and Supervisor](/en/blog/async-job-processing-with-laravel-queue-and-supervisor/) — Blog
- [My non-root container can't write to the mounted storage directory—how do I fix the permissions?](https://www.muhammetsafak.com.tr/en/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://www.muhammetsafak.com.tr/en/just-ask/should-i-author-the-openapi-spec-first-or-generate-it-from/) — Just Ask
- [How should I run the sunset process when deprecating an endpoint in my public API?](https://www.muhammetsafak.com.tr/en/just-ask/how-should-i-run-the-sunset-process-when-deprecating-an-endpoint/) — Just Ask
