GenerateSaaS

Purchases and refunds

Custom credit purchases, Stripe auto top-up, and how refunds and chargebacks reverse credits.

No customPurchase block ships, so custom purchases and auto top-up are off. Add pricingConfig.credits.customPurchase to let users buy credits beyond their plan:

KeyTypeDescription
enabledbooleanAllow custom credit purchases.
autoTopUpbooleanAllow users to opt into automatic refills.
minimumCreditsnumberFloor per custom purchase.
presetCreditsnumberSuggested amount in the purchase UI.
stripeProductId / polarProductIdstring?Metered line item per provider.

A top-up fires only when all hold: customPurchase.enabled and customPurchase.autoTopUp are on, the active provider is "stripe", the paying user has set autoTopUpEnabled/autoTopUpThreshold/autoTopUpAmount (threshold and amount must be > 0), and the fresh balance is below the threshold.

  • removeCredits (successful or insufficient) and settleCredits with cause: "usage" both run the check.
  • Refund and chargeback settlements never auto-top-up, so a reversal cannot charge the customer again.

Auto top-up is Stripe-only: under Polar users still buy credits manually, but no automatic refill runs. Asynchronous methods (SEPA/ACH) confirm days after the charge, so a pending-charge marker blocks further triggers and one low balance cannot mint multiple live charges. It clears on payment_intent.succeeded or payment_intent.payment_failed - enable both events - or after a 14-day backstop TTL.

Refunds and chargebacks

Prepaid credits are non-refundable, and a notice says so at the point of purchase. That is policy copy: it does not block a refund you issue from your provider dashboard, which settles the credit reversal below.

EventEffect
charge.refunded / order.refundedSettles the applicable credit reversal. Custom-credit, top-up, and subscription refunds reverse each newly refunded share; product and lifetime-plan benefits reverse once, on the first reversal event.
charge.dispute.created (Stripe)Settles the dispute's newly reversible share and suspends the buyer (Better Auth banned), ending their live sessions so a reversed payment stops spending immediately. An admin lifts the ban after review.
  • Already-spent credits become debt: a 100-credit reversal against a balance of 10 leaves -90, repaid naturally by later additions. Credits are one pooled balance with no per-source accounting, so a reversal consumes whatever is in it - credits bought separately for cash included.
  • The durable charge_reversals table tracks cumulative refunded and disputed amounts per charge, so repeated, partial, or out-of-order events can never reverse more than the original grant.

Enable charge.dispute.created in your Stripe webhook - it is off by default and it is the chargeback defense, since a disputing customer cannot be declined the way a refund request can. Polar is a merchant of record and handles disputes itself (a lost dispute arrives as a normal refund), so this handling is Stripe-only.

On this page