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:

  1. 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.
  2. 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_id and stripe_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:

  1. Stripe attempts the charge → fails → sends invoice.payment_failed.
  2. The system flips the member's payment_status to past_due. They show up in the Past due filter.
  3. Stripe retries on its own schedule (typically over 1–3 weeks).
  4. If a retry succeeds, Stripe sends invoice.paid, the system flips payment_status back to active, and the member drops out of the Past due filter automatically.
  5. If every retry fails, Stripe cancels the subscription and sends customer.subscription.deleted. The system flips payment_status to 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_events table 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?