Блог Ресурсы О нас Поиск Темы
Руководства

Платежи Polar.sh в Node.js: Полное руководство по бэкенду

Обновлено 3 января 2026 г.

Категория: Руководства
Поделиться

Интеграция Polar.sh платежей с Node.js бэкендом

Эта статья расширяет мою ленту в X о платежах Polar.sh с полными примерами кода, обработкой ошибок и уроками, извлеченными из производственного использования.

Если вы создаете SaaS или продаете цифровые продукты, вам нужен платежный провайдер. Polar.sh стал моим выбором для инди-проектов — вот как интегрировать его с Node.js бэкендом.

Почему Polar.sh? #why-polar-sh

Прежде чем погрузиться в код, вот почему я выбрал Polar.sh вместо альтернатив:

В сравнении со Stripe: Polar делает больше из коробки. Вы получаете хостинговые страницы оформления заказа, клиентский портал и управление жизненным циклом подписки без необходимости строить это самостоятельно. Stripe дает больше контроля, но требует больше кода.

В сравнении с Paddle/Lemon Squeezy: Похожие преимущества merchant of record (они обрабатывают НДС/налог с продаж), но Polar создан специально для разработчиков и open-source проектов. DX (Developer Experience) заметно лучше.

Что Polar обрабатывает за вас:

  • Хостинговые страницы оформления заказа
  • Обработку платежей
  • Клиентский портал
  • Жизненный цикл подписки
  • Налоговое соответствие (как merchant of record)

Что вы обрабатываете:

  • События вебхуков
  • Права пользователей (entitlements)
  • Отслеживание использования

Настройка проекта #project-setup

Установите SDK и библиотеку для верификации вебхуков:

Terminal
npm install @polar-sh/sdk standardwebhooks

Инициализируйте Polar клиент:

src/lib/polar.ts
import { Polar } from '@polar-sh/sdk';

export const polar = new Polar({
    accessToken: process.env.POLAR_ACCESS_TOKEN!,
    server: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
});

Необходимые переменные окружения:

.env
POLAR_ACCESS_TOKEN=your_access_token
POLAR_WEBHOOK_SECRET=your_webhook_secret

Получите их из вашего Polar dashboard. Используйте режим песочницы (sandbox) во время разработки — он предоставляет тестовые карты и не будет списывать реальные деньги.

Создание сессий оформления заказа #creating-checkout-sessions

Когда пользователь нажимает “Подписаться” или “Купить”, создайте сессию оформления заказа и перенаправьте его:

src/routes/checkout.ts
import { polar } from '../lib/polar';

export async function createCheckout(user: { id: string; email: string }, productId: string) {
    const checkout = await polar.checkouts.create({
        products: [productId],
        successUrl: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
        customerEmail: user.email,
        metadata: {
            userId: String(user.id),
        },
    });

    return checkout.url;
}

Ключевые моменты:

  • metadata.userId связывает Polar клиента с вашим пользователем. Это понадобится в вебхуках.
  • successUrl может включать {CHECKOUT_SESSION_ID}, который Polar заменяет на реальный ID.
  • products принимает массив — вы можете объединять несколько продуктов в одном оформлении.

Пример маршрута Express:

src/routes/api.ts
router.post('/create-checkout', async (req, res) => {
    const user = req.user; // из вашего middleware аутентификации
    const { productId } = req.body;

    if (!productId) {
        return res.status(400).json({ error: 'Требуется ID продукта' });
    }

    try {
        const checkoutUrl = await createCheckout(user, productId);
        res.json({ url: checkoutUrl });
    } catch (error) {
        console.error('Ошибка создания оформления:', error);
        res.status(500).json({ error: 'Не удалось создать оформление' });
    }
});

Обработка вебхуков #webhook-handling

Вебхуки — это то, как Polar сообщает вашему приложению о платежных событиях. Это самая критическая часть для правильной реализации.

Верификация подписи

Polar использует спецификацию Standard Webhooks. Вы должны проверять подписи, чтобы предотвратить поддельные запросы:

src/webhooks/polar.ts
import { Webhook } from 'standardwebhooks';

const secret = 'whsec_' + Buffer.from(
    process.env.POLAR_WEBHOOK_SECRET!
).toString('base64');

const wh = new Webhook(secret);

export function verifyWebhook(rawBody: string, headers: Record<string, string>) {
    try {
        return wh.verify(rawBody, headers);
    } catch (error) {
        console.error('Ошибка верификации вебхука:', error);
        throw new Error('Неверная подпись вебхука');
    }
}

Настройка маршрута (Критично!)

Маршрут вебхука требует сырого тела запроса для верификации подписи. Зарегистрируйте его ДО вашего JSON middleware:

src/app.ts
import express from 'express';
import { webhookRouter } from './routes/webhooks';

const app = express();

// Маршруты вебхуков ПЕРВЫМИ - нужен сырой body
app.use('/api/webhooks', webhookRouter);

// Парсинг JSON ПОСЛЕ вебхуков
app.use(express.json());

// Другие маршруты...
app.use('/api', apiRouter);
src/routes/webhooks.ts
import { Router } from 'express';
import express from 'express';
import { verifyWebhook } from '../webhooks/polar';
import { handlePolarEvent } from '../services/polar-events';

const router = Router();

router.post('/polar',
    express.raw({ type: 'application/json' }),
    async (req, res) => {
        try {
            const event = verifyWebhook(
                req.body.toString(),
                req.headers as Record<string, string>
            );

            await handlePolarEvent(event);
            res.status(200).json({ received: true });
        } catch (error) {
            console.error('Ошибка вебхука:', error);
            res.status(400).json({ error: 'Обработка вебхука не удалась' });
        }
    }
);

export { router as webhookRouter };

Обработчики событий

Вот как обрабатывать ключевые события подписки:

src/services/polar-events.ts
import { db } from '../lib/db';

interface PolarEvent {
    type: string;
    data: {
        id: string;
        status: string;
        customer_id: string;
        metadata?: { userId?: string };
        current_period_end?: string;
    };
}

export async function handlePolarEvent(event: PolarEvent) {
    const { type, data } = event;

    switch (type) {
        case 'subscription.created':
            // Связываем Polar клиента с вашим пользователем
            await db.user.update({
                where: { id: data.metadata?.userId },
                data: {
                    polarCustomerId: data.customer_id,
                    subscriptionId: data.id,
                    subscriptionStatus: 'pending',
                },
            });
            break;

        case 'subscription.active':
            // Платеж прошел - активируем подписку
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'active',
                    currentPeriodEnd: new Date(data.current_period_end!),
                    // Устанавливаем лимиты для конкретного тарифа
                    apiCallsLimit: 10000,
                    storageLimit: 5 * 1024 * 1024 * 1024, // 5GB
                },
            });
            break;

        case 'subscription.updated':
            // Смена тарифа, продление и т.д.
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: data.status,
                    currentPeriodEnd: new Date(data.current_period_end!),
                },
            });
            break;

        case 'subscription.canceled':
            // Пользователь отменил - доступ сохраняется до конца периода
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'canceled',
                    canceledAt: new Date(),
                },
            });
            break;

        case 'subscription.revoked':
            // Немедленный отзыв (неудачный платеж, возврат и т.д.)
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'revoked',
                    apiCallsLimit: 0,
                    storageLimit: 0,
                },
            });
            break;

        case 'order.paid':
            // Одноразовая покупка завершена
            await handleOneTimePurchase(data);
            break;

        default:
            console.log(`Необработанный тип события: ${type}`);
    }
}

async function handleOneTimePurchase(data: PolarEvent['data']) {
    // Предоставляем пожизненный доступ, кредиты и т.д.
    await db.user.update({
        where: { id: data.metadata?.userId },
        data: {
            lifetimeAccess: true,
        },
    });
}

Управление подписками #subscription-management

Отмена подписки

Позвольте пользователям отменять из вашего приложения (доступ сохраняется до конца периода):

src/services/subscriptions.ts
import { polar } from '../lib/polar';

export async function cancelSubscription(subscriptionId: string) {
    await polar.subscriptions.update({
        id: subscriptionId,
        subscriptionUpdate: {
            cancelAtPeriodEnd: true,
        },
    });
}

Клиентский портал

Polar предоставляет хостинговый портал, где клиенты могут управлять биллингом, обновлять платежные методы и просматривать счета:

src/services/subscriptions.ts
export async function getCustomerPortalUrl(polarCustomerId: string) {
    const session = await polar.customerSessions.create({
        customerId: polarCustomerId,
    });

    return session.customerPortalUrl;
}

Маршрут Express:

src/routes/api.ts
router.post('/billing-portal', async (req, res) => {
    const user = req.user;

    if (!user.polarCustomerId) {
        return res.status(400).json({ error: 'Подписка не найдена' });
    }

    try {
        const portalUrl = await getCustomerPortalUrl(user.polarCustomerId);
        res.json({ url: portalUrl });
    } catch (error) {
        console.error('Ошибка создания портала:', error);
        res.status(500).json({ error: 'Не удалось создать сессию портала' });
    }
});

Распространенные ошибки #common-pitfalls

После интеграции Polar в продакшене вот ошибки, которые я видел (и совершал):

1. JSON middleware до вебхуков

Если вы видите ошибки верификации подписи, это почти всегда причина. Сырое тело парсится express.json() до того, как ваш обработчик вебхуков его увидит.

Исправление: Регистрируйте маршруты вебхуков до express.json().

2. Не обработка всех состояний подписки

Подписка может быть: active, canceled, past_due, incomplete, trialing или revoked. Не проверяйте только active — обрабатывайте полный жизненный цикл.

3. Отсутствие метаданных

Если вы забудете передать userId в метаданных оформления, вы не сможете связать Polar клиента с вашим пользователем. Всегда включайте идентифицирующую информацию.

4. Не идемпотентные обработчики вебхуков

Polar может повторять вебхуки. Ваши обработчики должны быть идемпотентными — обработка одного и того же события дважды не должна ничего сломать.

// Плохо - создает дублирующие записи
await db.payment.create({ data: { orderId: event.data.id, ... } });

// Хорошо - upsert на основе уникального ID
await db.payment.upsert({
    where: { polarOrderId: event.data.id },
    create: { polarOrderId: event.data.id, ... },
    update: { ... },
});

5. Блокирующие ответы вебхуков

Конечные точки вебхуков должны отвечать быстро. Если вам нужно выполнить тяжелую обработку, подтвердите получение немедленно и обрабатывайте асинхронно:

router.post('/polar', express.raw({ type: 'application/json' }), async (req, res) => {
    const event = verifyWebhook(req.body.toString(), req.headers);

    // Отвечаем немедленно
    res.status(200).json({ received: true });

    // Обрабатываем асинхронно (используйте proper job queue в продакшене)
    setImmediate(() => handlePolarEvent(event));
});

Тестирование вашей интеграции #testing-your-integration

Используйте режим Sandbox

Среда песочницы Polar позволяет протестировать полный поток без реальных списаний:

const polar = new Polar({
    accessToken: process.env.POLAR_ACCESS_TOKEN,
    server: 'sandbox', // Тестовый режим
});

Тестовые карты

В песочнице используйте эти тестовые номера карт:

  • 4242 4242 4242 4242 - Успешный платеж
  • 4000 0000 0000 0002 - Карта отклонена

Локальное тестирование вебхуков

Используйте ngrok или аналоги для проброса вашего локального сервера:

ngrok http 3000

Затем установите ваш URL вебхука в Polar dashboard на: https://your-ngrok-url.ngrok.io/api/webhooks/polar

Проверьте вашу настройку

Чек-лист перед запуском:

  • Оформление создается успешно и перенаправляет на Polar
  • URL успеха перенаправляет обратно в ваше приложение
  • Верификация подписи вебхука проходит
  • Событие subscription.active активирует доступ пользователя
  • Клиентский портал загружается корректно
  • Отмена работает и устанавливает cancelAtPeriodEnd

Это полная интеграция платежей Polar.sh. Ключевая идея: позвольте Polar делать то, что он умеет хорошо (UI оформления, обработка платежей, клиентский портал), а сосредоточьтесь на обработке вебхуков и правах пользователей в вашем коде.

Подробнее смотрите в официальной документации Polar.

Категория Руководства
Поделиться

Получайте последние идеи об ИИ прямо в свой почтовый ящик

Будьте в курсе последних тенденций, учебников и отраслевых идей. Присоединитесь к сообществу разработчиков, которые доверяют нашему информационному бюллетню.

Только новые аккаунты. Отправляя свой адрес электронной почты, вы согласны с нашей Политика конфиденциальности