Payment providers retry webhooks until they get a 2xx. If your handler times out after writing to the database, the next retry applies the same event again.
The fix
- Verify the signature before doing anything else.
- Insert the provider's event id into a
processed_eventstable with a unique constraint — in the same transaction as the business change. - If the insert conflicts, return
200and stop.
insert into processed_events (provider, event_id) values ($1, $2)
on conflict do nothing
returning event_id;
No row returned means the event was already applied.