Referral Program — Stripe Integration
Reward Paths
Three reward types, three different Stripe mechanisms.
credit — Customer Balance Transaction
A customer.balance_transaction with a negative amount is booked to
the referrer Customer. Stripe automatically applies the credit to the
next invoice (negative position "Referral Credit").
- Referral duration: as long as the Stripe Customer exists.
- Multi-Currency: Stripe automatically converts to the Customer currency; we log the conversion rate in the referral notes.
- Treated as a pre-tax discount when Stripe Tax is active — the sales tax is reduced proportionally.
coupon — Subscription Discount
The configured Stripe Coupon is applied to the active subscription of
the referrer via subscription.update (field discounts). It affects
the next invoice.
- Important: Stripe does not stack two coupons; a new add replaces a previous coupon on the same subscription.
- Coupon must exist: when activating a new reward setup, test manually once (qualify advertisement on test tenant + grant).
trial_extension — Subscription Trial End Update
subscription.update with a new trial_end (old value + configured
days). proration_behavior=none to prevent mid-cycle calculation
from being triggered.
- Only effective if
subscription.status == trialing; converted subscriptions are no-op (grant routine should then fall back to credit — currently, the lifecycle status is set to rewarded withreward_dayssnapshot, but no Stripe mutation).
Idempotency
Each Stripe call carries a deterministic idempotency key:
- Grant:
referral_grant_<referral_id> - Clawback:
referral_clawback_<referral_id>
This means: a second cron tick or a manual click on "Pay out reward" produces no duplicate booking — Stripe recognizes the key and returns the same transaction.
Clawback
The customer.subscription.deleted webhook (or charge.refunded /
charge.dispute.created) calls ClawbackOnChurn(tenantID). For each
rewarded referral in the clawback window:
- Credit: reverse
balance_transactionwith a positive amount. - Coupon / Trial Extension: not retro-reversible — the referral is only marked as canceled locally + audited.
Stripe Outage
If the grant Stripe call fails:
- The referral remains on status Qualified (not Rewarded).
- The Notes column gets a timestamp + error reason.
- A
referral.grant_failedaudit event is written. - The next cron tick tries again — the idempotency key guarantees no double booking.
If a single referrer permanently does not have a Stripe Customer (e.g., manual subscription without Stripe setup): the grant fails every time. Solution: either manually create the Stripe Customer, or manually cancel the referral.
Coupon Conflicts
- Stripe Coupon has
max_redemptions=1and has already been redeemed elsewhere → grant fails, admin alert. - Stripe Coupon has been deleted → grant fails, admin alert.
- Stripe Coupon has expired (
redeem_bytimestamp exceeded) → grant fails.
In all three cases: the referral remains Qualified; the admin fixes the coupon and triggers the next cron tick (or manually clicks "Pay out reward" on the detail page).