MONTASER HUSSAM
0%
Note

A payment webhook will fire twice

26 May 20263 min readAll notes

Payment providers retry webhook delivery on any ambiguous response — a timeout, a 500, even a slow 200. That means the handler will, eventually, receive the same event twice, and a naive implementation will charge or credit the account twice.

The fix is small, but has to be there from day one

def handle_payment_webhook(event: dict):
    event_id = event["id"]
    if already_processed(event_id):
        return {"status": "duplicate_ignored"}
    with db.transaction():
        record_event(event_id)
        apply_payment(event)
    return {"status": "ok"}

An idempotency key checked inside the same transaction as the side effect — but it has to be there from the first webhook handler written, not added after the first duplicate charge is reported.

Discuss this note →