Skip to content
Integrations

Stripe

Wire Stripe Checkout, subscriptions, and webhooks into your FlareX app — from API keys to first paid customer.

Updated

Stripe is the most common integration on FlareX. This page covers the full flow: API keys, Checkout for one-time payments, subscription plans, and the webhook plumbing that keeps your app's state in sync with Stripe.

API keys

Two keys, two environments:

ModeKey prefixWhere to find
Testsk_test_…Dashboard top-right toggle "Test mode" → Developers → API keys
Livesk_live_…Same place, "Test mode" off

Always start in test mode. Flip to live keys only when you've confirmed the full flow works end-to-end.

Add to Secrets as STRIPE_SECRET_KEY. Don't ship the publishable key (pk_...) anywhere private — that one's safe in the browser.

Pattern 1: Checkout for one-time payments

Simplest possible payment flow. User clicks a button → redirected to Stripe-hosted checkout → redirected back when done.

Add Stripe Checkout. Use STRIPE_SECRET_KEY from Secrets.

POST /checkout creates a Checkout Session for the requested SKU and
returns the session URL. Frontend redirects the user to it.

success_url: /thanks?session_id={CHECKOUT_SESSION_ID}
cancel_url: /pricing

After redirect to /thanks, look up the session, confirm payment_status
is "paid", and render a confirmation page.

FlareX writes:

const session = await stripe.checkout.sessions.create({
  mode: 'payment',
  line_items: [{ price: 'price_…', quantity: 1 }],
  success_url: `${BASE_URL}/thanks?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${BASE_URL}/pricing`,
});
return { url: session.url };

Pattern 2: Subscriptions

Same shape, mode: 'subscription'. The user gets a recurring charge; you get a customer.subscription.created webhook to mirror state into your DB.

Add subscription Checkout. Plans:
  Starter: $9/mo  → STRIPE_PRICE_STARTER
  Pro:     $29/mo → STRIPE_PRICE_PRO

POST /subscribe takes a plan code, creates the session, returns the URL.

Mirror subscription state into a `subscriptions` table keyed by user_id.
Update the row on customer.subscription.created/updated/deleted webhooks.

Pattern 3: Webhooks

Stripe is the source of truth for billing state. Your DB is a mirror. Webhooks keep them in sync.

The minimum events:

  • checkout.session.completed — fires when Checkout finishes
  • customer.subscription.created / updated / deleted — subscription lifecycle
  • invoice.paid / invoice.payment_failed — for dunning

See the webhooks doc for the signature verification pattern. The Stripe-specific bit:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

app.post('/webhooks/stripe', { config: { rawBody: true } }, async (req, reply) => {
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      req.rawBody,
      req.headers['stripe-signature'] as string,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch {
    return reply.code(400).send();
  }

  switch (event.type) {
    case 'checkout.session.completed':
      // mark order as paid
      break;
    case 'customer.subscription.updated':
      // mirror sub state
      break;
  }
  reply.send({ received: true });
});
Heads up

The webhook secret (whsec_…) is per endpoint, not per account. Test-mode and live-mode have separate webhook endpoints with separate secrets. When you flip to live, register a new endpoint and store the new secret as STRIPE_WEBHOOK_SECRET.

Pattern 4: Customer portal

Don't write a billing UI yourself. Stripe ships one:

Add a /billing-portal endpoint. Look up the user's stripe_customer_id,
create a billingPortal session, return the URL. Frontend redirects to
it. Users can update payment method, cancel, view invoices.
const session = await stripe.billingPortal.sessions.create({
  customer: stripeCustomerId,
  return_url: `${BASE_URL}/account`,
});
return { url: session.url };

Idempotency

Stripe accepts an idempotency-key header on every mutation. If you hit a network blip mid-charge, retry with the same key and Stripe returns the original result instead of double-charging:

await stripe.charges.create(
  { amount: 999, currency: 'usd', customer: cusId },
  { idempotencyKey: orderId },
);

FlareX picks idempotency keys for you (typically the order id or a UUID per attempt). Mention idempotency explicitly if you have a custom requirement.

Test cards

Use these in test mode to exercise specific flows:

CardBehaviour
4242 4242 4242 4242Always succeeds
4000 0000 0000 0002Always declined
4000 0000 0000 9995Insufficient funds
4000 0027 6000 3184Requires 3DS authentication

Any future expiry, any CVC, any ZIP.

Going live

  1. Replace test keys with live keys

    Update STRIPE_SECRET_KEY in Secrets. Restart the app.

  2. Register live-mode prices

    Live and test prices are separate. Recreate the products + prices in live mode and update STRIPE_PRICE_* env vars.

  3. Register a live webhook endpoint

    Same URL, but in live-mode dashboard. Copy the new whsec_… into STRIPE_WEBHOOK_SECRET.

  4. Smoke test with a real card

    Create a Stripe coupon for 100% off, run yourself through the live flow once, then disable the coupon.

What's next

Stripe · FlareX