Back to the blog

Stripe Integration: Payments and Subscriptions

Complete guide to Stripe integration: checkout, payments, subscriptions, webhooks, invoices and error handling.

January 5, 2026
12 min read
518 views
MOLOTILO

MOLOTILO DIGITAL

Stripe Integration: Payments and Subscriptions

Why Stripe

Stripe is a leading payment platform. It supports cards, Apple Pay, Google Pay, subscriptions and much more. Excellent documentation and SDKs for every language.

Installation and setup

npm install stripe @stripe/stripe-js
// lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
  typescript: true
});

// Client side
// lib/stripe-client.ts
import { loadStripe } from '@stripe/stripe-js';

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

Stripe Checkout

The simplest way to accept a payment is Stripe Checkout:

// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { items, userId } = await request.json();

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: items.map((item: CartItem) => ({
      price_data: {
        currency: 'rub',
        product_data: {
          name: item.name,
          images: [item.image],
          description: item.description
        },
        unit_amount: item.price * 100 // In kopecks
      },
      quantity: item.quantity
    })),
    mode: 'payment',
    success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
    metadata: {
      userId
    }
  });

  return NextResponse.json({ sessionId: session.id });
}

// Client component
'use client';

import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

function CheckoutButton({ items }: { items: CartItem[] }) {
  const handleCheckout = async () => {
    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ items, userId: 'user-123' })
    });

    const { sessionId } = await response.json();
    const stripe = await stripePromise;
    await stripe?.redirectToCheckout({ sessionId });
  };

  return (
    <button onClick={handleCheckout}>
      Pay
    </button>
  );
}

Subscriptions

// Creating a subscription
export async function POST(request: Request) {
  const { priceId, customerId } = await request.json();

  // Create or get a customer
  let customer = customerId;
  if (!customer) {
    const newCustomer = await stripe.customers.create({
      email: 'user@example.com',
      metadata: { userId: 'user-123' }
    });
    customer = newCustomer.id;
  }

  const session = await stripe.checkout.sessions.create({
    customer,
    payment_method_types: ['card'],
    line_items: [{
      price: priceId, // Price ID from the Stripe Dashboard
      quantity: 1
    }],
    mode: 'subscription',
    success_url: `${process.env.NEXT_PUBLIC_URL}/subscription/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`
  });

  return NextResponse.json({ sessionId: session.id });
}

// Canceling a subscription
export async function cancelSubscription(subscriptionId: string) {
  const subscription = await stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true
  });
  return subscription;
}

// Changing the plan
export async function updateSubscription(
  subscriptionId: string,
  newPriceId: string
) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);

  const updated = await stripe.subscriptions.update(subscriptionId, {
    items: [{
      id: subscription.items.data[0].id,
      price: newPriceId
    }],
    proration_behavior: 'create_prorations'
  });

  return updated;
}

Webhooks

// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';

export async function POST(request: Request) {
  const body = await request.text();
  const signature = headers().get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed');
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleSuccessfulPayment(session);
      break;
    }

    case 'invoice.paid': {
      const invoice = event.data.object as Stripe.Invoice;
      await handleInvoicePaid(invoice);
      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCanceled(subscription);
      break;
    }
  }

  return new Response('OK', { status: 200 });
}

async function handleSuccessfulPayment(session: Stripe.Checkout.Session) {
  const userId = session.metadata?.userId;

  await prisma.order.create({
    data: {
      userId,
      stripeSessionId: session.id,
      amount: session.amount_total! / 100,
      status: 'paid'
    }
  });

  // Send an email
  await sendOrderConfirmation(session.customer_email!);
}

Error handling

try {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: 'rub'
  });
} catch (error) {
  if (error instanceof Stripe.errors.StripeCardError) {
    // Card error
    console.log('Card declined:', error.message);
  } else if (error instanceof Stripe.errors.StripeRateLimitError) {
    // Rate limit exceeded
    console.log('Rate limit exceeded');
  } else if (error instanceof Stripe.errors.StripeInvalidRequestError) {
    // Invalid parameters
    console.log('Invalid request:', error.message);
  } else {
    // Other error
    console.log('Stripe error:', error);
  }
}

Conclusion

Stripe simplifies accepting payments and managing subscriptions. Checkout is a fast start, webhooks are for syncing data. Always verify webhook signatures and handle errors.

Test in Stripe Test Mode before production. Use test cards: 4242 4242 4242 4242.

Enjoyed the article?

Subscribe to our blog so you don’t miss new posts