The easiest way to integrate Stripe

Jan 26, 2025

Ayush, Autumn Co-Founder

This article is based on Theo Browne's method for integrating Stripe—huge credit to him. Follow him on Twitter @theo and check out his site.

Stripe's APIs are powerful, low-level blocks that enable us to bill in any way we want.

Unfortunately, this makes integration pretty painful. Theo Browne summarises the problem best:

IMO, the biggest issue with Stripe is the "split brain" it inherently introduces to your code base. When a customer checks out, the "state of the purchase" is in Stripe. You're then expected to track the purchase in your own database via webhooks.

There are over 258 event types. They all have different amounts of data. The order you get them is not guaranteed. None of them should be trusted. It's far too easy to have a payment be failed in stripe and "subscribed" in your app.

Overall Flow

Here's the easiest way of going about it, making use of the functions in his GitHub repo. The overall flow is:

  1. User clicks "Subscribe" button on frontend, triggering backend endpoint to generate Stripe checkout

  2. Backend creates Stripe customer, stores customerId/userId mapping in database

  3. Backend creates checkout session, returns it to frontend

  4. User completes payment, gets redirected to /success page

  5. /success page triggers backend to sync Stripe data for that customer

  6. Backend fetches customerId from database (referred to as key-value/KV store), and syncs latest data from Stripe API using syncStripeDataToKV

  7. After sync, frontend redirects user to final destination

  8. On all relevant Stripe webhook events, sync again via syncStripeDataToKV

Customer Checkout
  1. Authenticate user and get their ID

  2. Check database for existing Stripe customer ID for this user

  3. If no customer ID found, create a new Stripe customer and store the ID in the database, mapped to the user ID

  4. Create a Stripe Checkout session using the customer ID

  5. Return the session data to the frontend

export async function createCheckout(req: Request) {
  // Authenticate user and get their ID
  const user = auth(req);
  
  // Check database for existing Stripe customer ID
  let customerId = await db.get(`stripe:customer:${user.id}`);

  // If no customer ID found, create new Stripe customer 
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: user.email,
      metadata: {
        userId: user.id, //This is important!
      },
    });
    
    // Store mapping of Stripe customer to user ID
    await db.set(`stripe:customer:${user.id}`, customer.id);
    customerId = customer.id;
  }

  // Create Stripe Checkout session with customer ID
  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    // Other checkout options... 
  });
  
  // Return session data to frontend
  return session;
}
Sync Stripe Data

This is the function that will be used to sync Stripe subscription status with your application. We'll use it in the next step when we're processing webhook events.

  1. Fetch the latest subscription data for the given customer ID from Stripe's API

  2. If no subscriptions found, store an empty state object in the database

  3. If subscriptions found, extract the relevant fields (subscription ID, status, price ID, billing period, payment method details)

  4. Store the subscription data object in the database, keyed by the customer ID

  5. Return the subscription data

async function syncStripeDataToKV(customerId: string) {
  // Fetch latest subscription data from Stripe
  const subscriptions = await stripe.subscriptions.list({
    customer: customerId,
    limit: 1,
    status: "all",
    expand: ["data.default_payment_method"],
  });

  // If no subscriptions, store empty state and return
  if (subscriptions.data.length === 0) {
    const emptyData = { status: "none" };
    await db.set(`stripe:customer:${customerId}`, emptyData);
    return emptyData;
  }

  // Extract relevant subscription fields
  const subscription = subscriptions.data[0];
  const subscriptionData = {
    subscriptionId: subscription.id,
    status: subscription.status,
    priceId: subscription.items.data[0].price.id,
    currentPeriodEnd: subscription.current_period_end,
    currentPeriodStart: subscription.current_period_start,
    cancelAtPeriodEnd: subscription.cancel_at_period_end,
    paymentMethod: 
      subscription.default_payment_method?.card 
        ? {
          brand: subscription.default_payment_method.card.brand,
          last4: subscription.default_payment_method.card.last4,
        }
        : null,
  };

  // Store subscription data in database
  await db.set(`stripe:customer:${customerId}`, subscriptionData);

  // Return subscription data
  return subscriptionData;
}
Listening to webhook events
  1. Define a list of relevant subscription-related event types to handle

  2. When a webhook is received, call this function to check if the received event type is in the list of relevant events

  3. If so, extract the customer ID from the webhook payload

  4. Call the syncStripeDataToKV function to update the customer's subscription data in the database

export async function processWebhook(event: Stripe.Event) {
  // Define relevant event types to handle
  const relevantEvents = [
    "customer.subscription.created",
    "customer.subscription.updated", 
    "customer.subscription.deleted",
    // ... other events
  ];

  // Check if event is relevant 
  if (!relevantEvents.includes(event.type)) {
    return;
  }

  // Extract customer ID from webhook payload
  const subscription = event.data.object as Stripe.Subscription;
  const customerId = subscription.customer as string;

  // Sync latest subscription data to database
  await syncStripeDataToKV(customerId);
}
/success endpoint

While this isn't 'necessary', there's a good chance your user will make it back to your site before the webhooks do. It's a nasty race condition to handle. Eagerly calling syncStripeDataToKV will prevent any weird states you might otherwise end up in.

export async function GET(req: Request) {
  const user = auth(req);
  const stripeCustomerId = await kv.get(`stripe:user:${user.id}`);
  if (!stripeCustomerId) {
    return redirect("/");
  }

  await syncStripeDataToKV(stripeCustomerId);
  return redirect("/");
}
This doesn't solve everything…

Maintaining billing is hard. This guide only applies to simple subscriptions and there are still some things you have to deal with:

Managing STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY env vars for both testing and production

  • Managing STRIPE_PRICE_IDs for all subscription tiers for dev and prod (I can't believe this is still a thing)

  • Exposing sub data from your KV to your user (a dumb endpoint is probably fine)

  • Tracking "usage" (i.e. a user gets 100 messages per month)

  • Managing "free trials" ...the list goes on

Making this into 1 line of code

Transparently, we're plugging this article because we've built a system to abstract this complexity away. With Autumn, you can define any pricing model you want (subscriptions, usage-based, credits, add-ons etc) and handle all the logic with one line of code.

Autumn manages all the subscription states. Just make an API call to Autumn to find out whether a user can access a feature. We'll take care of the rest so you can spend more time building what matters.

Give Autumn a try! And let us know what you think hey@useautumn.com