Платежи 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 и библиотеку для верификации вебхуков:
npm install @polar-sh/sdk standardwebhooks
Инициализируйте Polar клиент:
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',
});
Необходимые переменные окружения:
POLAR_ACCESS_TOKEN=your_access_token
POLAR_WEBHOOK_SECRET=your_webhook_secret
Получите их из вашего Polar dashboard. Используйте режим песочницы (sandbox) во время разработки — он предоставляет тестовые карты и не будет списывать реальные деньги.
Создание сессий оформления заказа #creating-checkout-sessions
Когда пользователь нажимает “Подписаться” или “Купить”, создайте сессию оформления заказа и перенаправьте его:
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:
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. Вы должны проверять подписи, чтобы предотвратить поддельные запросы:
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:
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);
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 };
Обработчики событий
Вот как обрабатывать ключевые события подписки:
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
Отмена подписки
Позвольте пользователям отменять из вашего приложения (доступ сохраняется до конца периода):
import { polar } from '../lib/polar';
export async function cancelSubscription(subscriptionId: string) {
await polar.subscriptions.update({
id: subscriptionId,
subscriptionUpdate: {
cancelAtPeriodEnd: true,
},
});
}
Клиентский портал
Polar предоставляет хостинговый портал, где клиенты могут управлять биллингом, обновлять платежные методы и просматривать счета:
export async function getCustomerPortalUrl(polarCustomerId: string) {
const session = await polar.customerSessions.create({
customerId: polarCustomerId,
});
return session.customerPortalUrl;
}
Маршрут Express:
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.