Stripe & webhooks

Gringotts sells credits through your Stripe account with Stripe Checkout, and keeps balances correct as payments settle, get refunded, or are disputed.

Configuration

Set two secrets (directly on GringottsConfig or via the environment):

export STRIPE_SECRET_KEY=sk_live_...
export STRIPE_WEBHOOK_SECRET=whsec_...

Define your packs in code — no Stripe Dashboard product setup is needed:

GringottsConfig(
    packs=[
        CreditPack(credits=100, price_cents=500, name="Starter"),
        CreditPack(credits=1000, price_cents=4000, name="Pro"),
    ],
    stripe_secret_key="sk_live_...",
    stripe_webhook_secret="whsec_...",
)

All packs must share one currency.

Webhook events to register

Point a Stripe webhook at POST {mount_path}/webhook and subscribe to:

Event

Why

checkout.session.completed

Grant credits once a payment is paid.

checkout.session.async_payment_succeeded

Delayed methods (e.g. ACH) settle after the completed event — credit only then.

refund.created, refund.updated

Claw back a proportional share of the granted credits when a payment is refunded.

charge.dispute.funds_withdrawn

Claw back the purchase when a dispute pulls funds.

charge.dispute.funds_reinstated

Re-credit if the dispute is later resolved in your favor.

Every event is signature-verified against STRIPE_WEBHOOK_SECRET; an invalid signature is rejected with 400.

Crediting

Credits are granted only once the checkout session’s payment_status is paid (or no_payment_required). Before redirecting the buyer, Gringotts persists the authorized user, credits, amount, and currency in checkout_orders. Fulfillment requires the signed Session to match that local order exactly; unrelated Checkout integrations on the same Stripe account are ignored. Idempotency is keyed on the checkout session id, so Stripe re-delivering an event—or sending more than one event for the same session—never double-credits.

Clawback (refunds and disputes)

When a payment is reversed, gringotts reverses the credits it granted:

  • Refunds claw back proportionally: a refund of R of a payment of C that granted N credits reverses round(N × R C). Multiple partial refunds are tracked cumulatively, so rounding never over- or under-claws.

  • Disputes claw back on funds_withdrawn and re-credit on funds_reinstated. Inquiry (warning_*) events move no money and are ignored.

  • Clawback is clamped at zero — it never drives a balance negative, deducting only the credits the user still holds. When it can’t fully recover, it logs a warning so you can follow up.

  • It’s idempotent on the Stripe Refund/Dispute id, and correlates events to the original purchase by the PaymentIntent recorded at Checkout time.

Zero-decimal currencies

price_cents is the amount in the currency’s smallest unit. For zero-decimal currencies (JPY, KRW, and others), that unit is the whole currency — price_cents=500 means ¥500, not ¥5.00 — and gringotts displays it accordingly. Purchase, refund, dispute, and reinstatement rows retain the ISO currency, so historical revenue is totaled separately per currency.

Local testing

stripe listen --forward-to localhost:8000/gringotts/webhook

Upgrading

Run gringotts migrate after upgrading. Drain Checkout Sessions created by 0.3.x before moving to 0.4.0; the new release fulfills only Sessions backed by a local checkout_orders row. Two older caveats remain:

  • Purchases recorded by 0.1.x were keyed on the Stripe event id, not the checkout session. Drain any in-flight delayed payments before upgrading so a settlement arriving afterward isn’t credited twice.

  • Clawback correlates only purchases made on 0.3.0+ (older rows have no stored PaymentIntent); a refund or dispute on a pre-0.3 purchase is logged, not auto-clawed.