Online payments with Stripe
How Stripe Checkout handles paid memberships, what each billing kind looks like to the member, and what past_due means.
Overview
StatsCentral processes every online membership payment through Stripe Checkout. Members enter their card details on Stripe's hosted page — never on StatsCentral — and Stripe charges them on the cycle defined by the membership type. The app keeps a local mirror of payment state (active, past due, cancelled) by listening to Stripe webhooks, so your admin views always reflect what Stripe last reported.
You don't need a Stripe account beyond the one you connected during onboarding. Once a paid membership type exists and a member is on it, the rest is automatic.
How a member reaches Stripe Checkout
There are two entry points:
- From the join flow. When a new applicant picks a paid membership type on your public join page, the system creates a Checkout session for them and redirects to Stripe.
- From an invoice email. When you raise an invoice manually or the system raises one for a renewal, the member receives an email with a Pay now link. That link opens a fresh Checkout session for the same invoice.
Each session is created with an idempotency key scoped to the member and membership type, so refreshing or retrying produces the same session rather than duplicate charges.
After a successful payment Stripe redirects the member back to a confirmation page on your club. After a cancelled or abandoned payment they land on the same page they came from — the membership type is still attached, but no charge happens.
What the member sees per billing kind
Stripe Checkout adapts to the membership type's billing period:
| Billing period | Stripe mode | What the member sees |
|---|---|---|
| Annual subscription | subscription |
"USD 120 per year" with a recurring badge. Card is saved and charged again each year. |
| Monthly subscription | subscription |
"USD 15 per month" with a recurring badge. Card is saved and charged again each month. |
| One-time, pay in full | payment |
A single charge for the full amount. No saved card, no recurrence. |
| One-time, installment plan | subscription |
"USD 100 per month for 6 months" — Stripe shows the per-installment amount and the end date. Stripe auto-cancels the subscription after the final charge. |
For installment plans, the system passes a cancel_at to Stripe equal to the start time plus the number of months on the plan. There is nothing for you or the member to cancel — once the last installment charges, the subscription ends automatically.
What the system mirrors back from Stripe
Stripe is the source of truth for payment state. Webhooks fire after every relevant event, and StatsCentral updates the member record so admin views match what Stripe sees:
payment_status— mirrors the Stripe subscription status: active, past_due, cancelled, unpaid, etc.current_period_end— the next renewal date (or the end date of an installment plan) from Stripe.installments_remaining— for installment plans, decremented each time a monthly invoice is paid; reaches 0 when the plan completes.stripe_subscription_idandstripe_customer_id— IDs you can use to look the member up in your Stripe dashboard.
These fields appear on the member detail page and feed the Past due filter on the members list.
What past_due means
When a recurring charge fails — expired card, insufficient funds, bank decline — Stripe doesn't immediately give up. It moves the subscription to past_due and retries the card on its smart retry schedule (configured in the Stripe dashboard, not in StatsCentral).
Here's the sequence on the member record:
- Stripe attempts the charge → fails → sends
invoice.payment_failed. - The system flips the member's
payment_statusto past_due. They show up in the Past due filter. - Stripe retries on its own schedule (typically over 1–3 weeks).
- If a retry succeeds, Stripe sends
invoice.paid, the system flipspayment_statusback to active, and the member drops out of the Past due filter automatically. - If every retry fails, Stripe cancels the subscription and sends
customer.subscription.deleted. The system flipspayment_statusto cancelled.
You don't have to do anything during the retry window. Reach out to past-due members if you want — Stripe will email them as well, with a link to update their card on their hosted billing portal.
Webhook idempotency
Stripe occasionally re-delivers the same webhook (after a transient timeout, after you replay one from the dashboard, etc.). The system records every processed event in the stripe_events table keyed by Stripe's event ID and skips any event it has already seen. That means:
- Replaying a webhook from the Stripe dashboard does not re-charge the member or double-write history.
- Two webhooks for the same event arriving milliseconds apart only update the member once.
- If you ever need to confirm whether a specific Stripe event reached the app, search the
stripe_eventstable by event ID.
payment_status vs member status
These are two independent fields and they don't track each other:
status— the member's overall lifecycle: pending, active, expired, suspended, rejected.payment_status— only describes Stripe state: active, past_due, cancelled, unpaid.
A member can be active but past_due (their card just got declined — they keep their access during the retry window). A member can be expired but with payment_status: cancelled (you let the membership lapse on purpose). When deciding access, use status. When deciding whether Stripe is happy, use payment_status.
Edge cases / gotchas
- No card data on StatsCentral, ever. Cards live with Stripe; StatsCentral only stores the Stripe customer and subscription IDs. Members update their card via Stripe's hosted billing portal, not in our admin.
- Free (zero-amount) types skip Stripe entirely. The Checkout builder rejects any membership type with an amount of 0.00. Members on free types are activated without a payment step.
- Retry behaviour is owned by Stripe. If you want shorter or longer retry windows, change the smart retry settings in your Stripe dashboard — there is no setting for it in StatsCentral.
- Currency mismatches don't auto-convert. A type priced in PHP is charged in PHP. If your Stripe account doesn't support that currency, Checkout will fail before it even loads — verify your payout currency in the Stripe dashboard before launching new types.
- Cancelled ≠ refunded. Cancelling a subscription stops future charges; it does not refund anything already paid. Issue refunds from the Stripe dashboard if needed.
When a member has a payment problem, look at payment_status first, then their last invoice in Stripe — that's almost always faster than trying to debug it from the app side.
Was this helpful?
Thanks for the feedback!