2026-03-12 · 7 min read
Designing idempotent payment webhooks in Laravel
Payment gateways love retries. Sticky.io, Konnektive, and CheckoutChamp will happily POST the same capture event twice when latency spikes. If your handler is not idempotent, you invent double charges and angry finance tickets.
The pattern I use in Laravel: derive a stable event_key from gateway_id + event_type + transaction_ref, insert it into a unique webhook_events table inside a DB transaction, and only then mutate orders/subscriptions.
If the unique insert fails, return 200 quickly — the work already happened. Pair this with an outbox table for side effects (CRM sync, email) so retries never skip business events while still avoiding duplicates.
Add signature verification, timestamp skew checks, and structured logs with the event_key. When ops asks “did we charge twice?”, you can answer in seconds.