API reference

Billing & Stripe

Checkout, Portal, and webhook handling for the three subscription tiers.

Stripe powers Scout / Operator / Overlord subscriptions.

Env

STRIPE_SECRET_KEY=sk_live_...   (or sk_test_...)
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_SCOUT=price_...
STRIPE_PRICE_OPERATOR=price_...
STRIPE_PRICE_OVERLORD=price_...

Endpoints

POST /billing/checkout
  body: { tier: 'SCOUT' | 'OPERATOR' | 'OVERLORD' }
  → { url: <Stripe Checkout URL> }

POST /billing/portal
  → { url: <Stripe Customer Portal URL> }

POST /billing/webhook
  (Stripe-signed; no session auth)
  → 200 / 400

Webhook events handled

EventEffect
checkout.session.completedSet Subscription.status = ACTIVE, link stripeSubscriptionId.
customer.subscription.updatedRefresh tier, status, currentPeriodEnd, cancelAtPeriodEnd.
customer.subscription.deletedSet status CANCELED.
invoice.payment_failedSet status PAST_DUE.
customer.subscription.trial_will_end(logged; no action)

Replay protection

Every processed Stripe event ID is recorded in StripeWebhookEvent. Replays are no-ops.

Idempotency

Stripe's webhook delivery has at-least-once semantics. Our DB writes use upsert with the Stripe customer + subscription IDs as natural keys.

Refunds

The admin endpoint POST /admin/refund calls stripe.refunds.create() and records issuedBy (your Steam ID) and issuedAt in Stripe metadata. The customer's tier is not auto-downgraded on refund — the subsequent customer.subscription.updated webhook handles that if the subscription is cancelled by the refund flow.

Beta-tester precedence

If a user has both a Stripe sub and an active beta entry, the Stripe sub wins for tier resolution. This means staff can comp users without worrying about overriding their paid sub.