My Stripe delivery script silently skipped a paid customer for 7 days

python dev.to

The bug was one line of code. It cost a customer 7 days of waiting.

I run a small shop that sells Claude Code skill packs. When someone buys, a Python script polls Stripe for new charges and sends the product zip via Resend. Simple enough.

Here's what the original delivery loop looked like:

for charge in charges:
    if charge.id in processed:
        continue
    try:
        send_delivery_email(to_email, zip_filename, charge.id)
    except Exception as e:
        log.error("Delivery error: %s", e)

    processed.add(charge.id)  # BUG: runs even if delivery failed
    save_state(state)
Enter fullscreen mode Exit fullscreen mode

Can you see it?

processed.add(charge.id) is outside the try/except. It runs regardless of whether the delivery succeeded or failed. If Resend returned a 500, if the zip file was missing, if there was a network timeout — the charge gets marked as processed anyway.

The next poll run? Skipped. Silently. No retry. No alert. Just gone.

What actually happened

A developer bought my Ship Fast Skill Pack. The product zip wasn't staged on the delivery server. The send function returned False. The code marked the charge as processed, saved state, and moved on.

Seven days later I discovered it when I manually checked the Stripe dashboard and noticed a paid charge with no corresponding delivery log entry.

The fix

delivery_ok = False
try:
    delivery_ok = send_delivery_email(
        to_email, product_name, zip_filename, setup_instructions, charge_id
    )
except Exception as e:
    log.error("Unexpected delivery error for charge %s: %s", charge_id, e)

if delivery_ok:
    processed.add(charge_id)  # Only mark processed on confirmed delivery
    save_state(state)
else:
    failed_counts[charge_id] = failed_counts.get(charge_id, 0) + 1
    save_state(state)
Enter fullscreen mode Exit fullscreen mode

Now the charge only enters the processed set after Resend returns 200/201. Failed deliveries go into failed_counts and retry on each poll until they hit 3 failures — at which point they're flagged for manual review.

The pattern this breaks

This is the post-try unconditional write pattern. It shows up everywhere:

try:
    do_thing()
except:
    log_error()
mark_as_done()  # ← always runs
Enter fullscreen mode Exit fullscreen mode

It's seductive because it feels safe — the try/except handles failures, right? But "handling" a failure by logging it and continuing doesn't mean the downstream consumers of your state should treat it as a success.

The safe version: only advance state on confirmed success.

success = do_thing()
if success:
    mark_as_done()
Enter fullscreen mode Exit fullscreen mode

The tradeoff

Moving mark_as_done() inside the success branch means you need to be explicit about what "success" means. A return value? A status code check? No exception raised? There's no universal answer, but it forces you to be deliberate instead of implicit.

The implicit version is fast to write and wrong in production. The explicit version takes 30 seconds longer to write and catches bugs like this one.


Stop writing Stripe polling scripts from scratch. The Ship Fast Skill Pack includes a pre-wired stripe-payments skill that handles the webhook + fulfillment loop in a single Claude Code invocation. One-time $49 → https://whoffagents.com/products/ship-fast-skill-pack

Source: dev.to

arrow_back Back to Tutorials