Senior Application Developer

loc: Maynooth, ON K0L 2S0tel: 613-553-0960email: rgsamways@gmail.com
Robin Samways

Farpost · Payments

$ The Webhook Redelivery Stripe Warns You About

Stripe is explicit that it doesn't guarantee a webhook is delivered exactly once — the same event can arrive twice. Farpost's handler for a paid invoice was overwriting a job's paid-timestamp every single time that event arrived, including on a redelivery of an event it had already processed correctly.

That meant a redelivered, already-handled webhook could silently corrupt a record that was already correct — replacing an accurate paid-date with a later, wrong one, with nothing in the logs to suggest anything had gone wrong.

async def _handle_invoice_paid(invoice) -> None:
    """Mark platform_fee_collected on the job when the invoice is settled.

    Stripe does not guarantee at-most-once webhook delivery — a redelivered
    invoice.payment_succeeded was silently overwriting platform_fee_paid_at with a
    later timestamp on every redelivery, corrupting the actual paid-date. Now a
    no-op once already marked collected."""
    invoice_id = invoice["id"]
    job = await Job.find_one({"payment.platform_fee_invoice_id": invoice_id})
    if not job:
        return

    if job.payment.platform_fee_collected:
        return  # already recorded — a redelivery, not a new event

    job.payment.platform_fee_collected = True
    job.payment.platform_fee_paid_at = datetime.now(timezone.utc)
    job.payment.status = PaymentStatus.PAID
    await job.save()

The fix

The handler now checks whether the job is already marked platform_fee_collected before doing anything else, and returns immediately if so. A redelivery of an event that already succeeded becomes a no-op instead of a second write, so the paid-date it recorded the first time can't be overwritten by a later, incorrect one.

Why this matters

It's easy to build the happy path for a webhook — event arrives, update the record — and never consider what a second, redundant delivery of the exact same event does to that same update. Recognizing that a webhook handler needs to be idempotent, not just correct on a single delivery, is a specific and easy-to-skip form of defensive design.

Feedback on this page