Назад к блогу
Backend

Интеграция Stripe: платежи и подписки

Полное руководство по интеграции Stripe: checkout, платежи, подписки, webhooks, invoice и обработка ошибок.

5 января 2026 г.
12 мин чтения
151 просмотров
MOLOTILO

MOLOTILO DIGITAL

Интеграция Stripe: платежи и подписки

Почему Stripe

Stripe — ведущая платформа для приёма платежей. Поддерживает карты, Apple Pay, Google Pay, подписки и многое другое. Отличная документация и SDK для всех языков.

Установка и настройка

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
});

// Клиентская часть
// lib/stripe-client.ts
import { loadStripe } from '@stripe/stripe-js';

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

Stripe Checkout

Самый простой способ принять платёж — 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 // В копейках
      },
      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 });
}

// Клиентский компонент
'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}>
      Оплатить
    </button>
  );
}

Подписки

// Создание подписки
export async function POST(request: Request) {
  const { priceId, customerId } = await request.json();

  // Создаём или получаем 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, // ID цены из 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 });
}

// Отмена подписки
export async function cancelSubscription(subscriptionId: string) {
  const subscription = await stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true
  });
  return subscription;
}

// Изменение плана
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'
    }
  });
  
  // Отправляем email
  await sendOrderConfirmation(session.customer_email!);
}

Обработка ошибок

try {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: 'rub'
  });
} catch (error) {
  if (error instanceof Stripe.errors.StripeCardError) {
    // Ошибка карты
    console.log('Card declined:', error.message);
  } else if (error instanceof Stripe.errors.StripeRateLimitError) {
    // Превышен лимит запросов
    console.log('Rate limit exceeded');
  } else if (error instanceof Stripe.errors.StripeInvalidRequestError) {
    // Неверные параметры
    console.log('Invalid request:', error.message);
  } else {
    // Другая ошибка
    console.log('Stripe error:', error);
  }
}

Заключение

Stripe упрощает приём платежей и управление подписками. Checkout — быстрый старт, webhooks — для синхронизации данных. Всегда проверяйте подписи webhooks и обрабатывайте ошибки.

Тестируйте в Stripe Test Mode перед production. Используйте тестовые карты: 4242 4242 4242 4242.

StripeПлатежиПодпискиE-commerceAPI

Понравилась статья?

Подпишитесь на наш блог, чтобы не пропустить новые материалы