@besales/robokassa-sdk
v0.1.1
Published
SDK клиент для Robokassa Payment Microservice (server-side only)
Readme
@besales/robokassa-sdk
TypeScript SDK для интеграции с микросервисом обработки платежей через Robokassa.
SERVER-SIDE ONLY — не использовать в браузере. SDK работает с API-ключами, которые нельзя раскрывать на клиенте.
Установка
yarn add @besales/robokassa-sdk
# или
npm install @besales/robokassa-sdkТребования: Node.js >= 18.0.0 (используется нативный fetch)
Быстрый старт
import { RobokassaPaymentClient } from '@besales/robokassa-sdk';
const client = new RobokassaPaymentClient({
baseUrl: 'https://payments.example.com',
apiKey: process.env.PAYMENT_SERVICE_API_KEY!, // глобальный ключ
clientApiKey: process.env.PAYMENT_SERVICE_CLIENT_KEY!, // ключ клиента
});
// Одноразовый платёж
const { paymentLink, paymentId } = await client.payments.create({
userId: 'user-uuid',
productId: 'product-uuid',
});
// → redirect пользователя на paymentLink
// Подписка
const result = await client.subscriptions.create({
userId: 'user-uuid',
planId: 'plan-uuid',
});
if (result.requiresPayment) {
// → redirect на result.paymentLink
}Двойная аутентификация
SDK использует два API-ключа:
| Ключ | Откуда взять | Для чего |
|------|-------------|----------|
| apiKey | Переменная окружения API_KEY на сервере платежей | CRUD клиентов, продуктов, планов; создание платежей и подписок; списки; статистика |
| clientApiKey | Поле apiKey из таблицы Client (создаётся в админке) | Получение/отмена/пауза/возобновление конкретных подписок |
// Методы, требующие Global API Key (apiKey)
client.clients.create(...) client.products.list()
client.plans.create(...) client.payments.create(...)
client.subscriptions.create(...)
client.subscriptions.list(...)
// Методы, требующие Client API Key (clientApiKey)
client.subscriptions.getById(id)
client.subscriptions.cancel(id, userId)
client.subscriptions.pause(id, userId)
client.subscriptions.resume(id, userId)Если clientApiKey не передан в конструктор, вызов методов с auth: 'client' бросит ошибку.
Модули
Клиенты (clients)
// Список всех клиентов
const clients = await client.clients.list();
// Создать клиента
const newClient = await client.clients.create({
name: 'my_bot',
apiUrl: 'https://my-bot.example.com',
apiKey: 'webhook-secret-key',
});
// Получить по ID
const bot = await client.clients.getById('client-id');
// Обновить
await client.clients.update('client-id', { apiUrl: 'https://new-url.com' });
// Удалить (ошибка если есть привязанные продукты)
await client.clients.delete('client-id');Продукты (products)
Цена продуктов указывается в рублях.
// Все продукты
const products = await client.products.list();
// Продукты конкретного клиента
const botProducts = await client.products.listByClient('my_bot');
// Создать
const product = await client.products.create({
name: 'Пакет 100 генераций',
price: 999, // 999 рублей
generations: 100,
clientName: 'my_bot',
});Тарифные планы (plans)
Цена планов указывается в копейках (99900 = 999 рублей).
// Список с пагинацией
const { data: plans, total, page, limit } = await client.plans.list({
clientName: 'my_bot',
isActive: true,
page: 1,
limit: 20,
});
// Создать план
const plan = await client.plans.create({
name: 'Pro подписка',
price: 99900, // 999 рублей (в копейках!)
interval: 'MONTHLY',
generations: 1000,
clientName: 'my_bot',
trialDays: 7, // 7 дней бесплатного триала
});
// Обновить (PATCH, не PUT)
await client.plans.update('plan-id', { price: 149900 });
// Удалить/деактивировать
const result = await client.plans.delete('plan-id');
// result.deleted или result.deactivatedПлатежи (payments)
// Создать платёж
const { paymentLink, paymentId } = await client.payments.create({
userId: 'user-uuid',
productId: 'product-uuid',
language: 'ru',
robokassaConfigId: 'clp...', // опционально
});
// → redirect пользователя на paymentLink
// Обновить данные платежа (до оплаты)
await client.payments.update(paymentId, { username: 'john_doe' });Подписки (subscriptions)
// Создать подписку
const result = await client.subscriptions.create({
userId: 'user-uuid',
planId: 'plan-uuid',
language: 'ru',
robokassaConfigId: 'clp...', // опционально
});
if (result.requiresPayment) {
// Подписка без триала — нужна оплата
// redirect на result.paymentLink
} else {
// Подписка с триалом — уже активна
}
// Список подписок
const { data: subs, count } = await client.subscriptions.list({
userId: 'user-uuid',
status: 'ACTIVE',
page: 1,
limit: 20,
});
// Получить по ID (требует clientApiKey)
const sub = await client.subscriptions.getById('sub-id');
// Отменить (требует clientApiKey)
await client.subscriptions.cancel('sub-id', 'user-id');
// Приостановить (требует clientApiKey)
await client.subscriptions.pause('sub-id', 'user-id', 2); // на 2 месяца
// Возобновить (требует clientApiKey)
await client.subscriptions.resume('sub-id', 'user-id');
// Статистика
const stats = await client.subscriptions.getStats();
const cronStats = await client.subscriptions.getCronStats();Health check
const health = await client.healthcheck();
// { status: 'ok', service: 'payment-service', timestamp: '...' }
const subHealth = await client.subscriptions.health();Webhook-обработчики
SDK предоставляет готовые обработчики webhook'ов с валидацией API-ключа.
Платёжный сервис отправляет два типа webhook'ов на ваш сервис:
POST {apiUrl}/api/user/webhook— уведомления о платежахPOST {apiUrl}/api/user/subscription-webhook— изменения статуса подписок
Express
import { createExpressWebhookMiddleware } from '@besales/robokassa-sdk/webhooks';
// Webhook платежей
app.post('/api/user/webhook', ...createExpressWebhookMiddleware({
apiKey: process.env.WEBHOOK_API_KEY!,
onPayment: async (payload) => {
console.log('Платёж:', payload.paymentId, payload.status);
console.log('Генерации:', payload.generationsAdded);
console.log('Мультикасса:', payload.robokassaCode);
if (payload.status === 'PAID') {
await addGenerations(payload.userId, payload.generationsAdded);
}
},
}));
// Webhook подписок
app.post('/api/user/subscription-webhook', ...createExpressWebhookMiddleware({
apiKey: process.env.WEBHOOK_API_KEY!,
onSubscription: async (payload) => {
console.log('Подписка:', payload.subscriptionId, payload.status);
if (payload.status === 'CANCELLED') {
await deactivateUser(payload.userId);
}
},
}));Fastify
import { robokassaWebhookPlugin } from '@besales/robokassa-sdk/webhooks';
fastify.register(robokassaWebhookPlugin, {
apiKey: process.env.WEBHOOK_API_KEY!,
onPayment: async (payload) => {
await addGenerations(payload.userId, payload.generationsAdded);
},
onSubscription: async (payload) => {
if (payload.status === 'CANCELLED') {
await deactivateUser(payload.userId);
}
},
});Framework-agnostic (core)
import {
validateWebhookApiKey,
parsePaymentWebhook,
parseSubscriptionWebhook,
} from '@besales/robokassa-sdk/webhooks';
// В любом фреймворке
function handleRequest(headers: Record<string, string>, body: unknown) {
if (!validateWebhookApiKey(headers, 'expected-key')) {
return { status: 401 };
}
const payload = parsePaymentWebhook(body);
// ... обработка
}Обработка ошибок
SDK предоставляет типизированные ошибки:
import {
RateLimitError,
NotFoundError,
ValidationError,
UnauthorizedError,
ConflictError,
OutcomeUnknownError,
} from '@besales/robokassa-sdk';
try {
await client.payments.create({ userId: 'uuid', productId: 'uuid' });
} catch (error) {
// API-ошибки (наследуются от PaymentApiError)
if (error instanceof RateLimitError) {
// 429 — превышен rate limit
console.log(`Повторите через ${error.retryAfterMs}ms`);
} else if (error instanceof NotFoundError) {
// 404 — продукт или пользователь не найден
} else if (error instanceof ValidationError) {
// 400 — невалидные входные данные
} else if (error instanceof UnauthorizedError) {
// 401 — неверный API-ключ
} else if (error instanceof ConflictError) {
// 409 — конфликт (дубликат подписки, имя занято)
}
// ⚠️ OutcomeUnknownError НЕ наследует PaymentApiError!
// Это отдельный тип — «результат запроса неизвестен», не «API вернул ошибку».
// catch (e instanceof PaymentApiError) его НЕ поймает.
if (error instanceof OutcomeUnknownError) {
// Сетевая ошибка на POST/PATCH/DELETE
// Запрос мог выполниться, а мог нет — проверьте через GET перед повтором!
console.log('Оригинальная ошибка:', error.originalError);
}
}Retry-политика
| Метод | Retry при сетевой ошибке | Retry при 429 | Почему |
|-------|--------------------------|---------------|--------|
| GET | Автоматически (2 попытки) | Автоматически | Безопасно, идемпотентно |
| POST | Нет → OutcomeUnknownError | Нет → RateLimitError | Нет idempotency keys — может создать дубликат |
| PATCH | Нет → OutcomeUnknownError | Нет → RateLimitError | Нет гарантий идемпотентности |
| DELETE | Нет → OutcomeUnknownError | Нет → RateLimitError | Может выполниться дважды |
Принудительный retry для mutating-запроса (только если уверены):
// Opt-in retry для POST
const result = await client.someModule.someMethod(data, { retry: true });Конфигурация
const client = new RobokassaPaymentClient({
baseUrl: 'https://payments.example.com', // Обязательный
apiKey: 'global-api-key', // Обязательный
clientApiKey: 'client-api-key', // Для операций с подписками
language: 'ru', // Язык ответов: 'ru' | 'en'
timeout: 10_000, // Таймаут в мс (дефолт: 10000)
retries: 2, // Retry для GET (дефолт: 2)
});Webhook payload'ы
PaymentWebhookPayload
| Поле | Тип | Описание |
|------|-----|----------|
| paymentId | string | ID платежа |
| userId | string \| null | ID пользователя |
| status | string | Статус: PAID, FAILED |
| amount | number \| null | Сумма в рублях |
| generationsAdded | number | Количество начисленных генераций |
| productName | string | Название продукта |
| robokassaCode | string | Код Robokassa-конфигурации |
| isRecurring | boolean? | Рекуррентный платёж? |
| subscriptionType | string? | Тип подписки (MONTHLY и т.д.) |
| subscriptionDays | number? | Дней подписки |
SubscriptionWebhookPayload
| Поле | Тип | Описание |
|------|-----|----------|
| subscriptionId | string | ID подписки |
| userId | string | ID пользователя |
| status | string | Статус: ACTIVE, CANCELLED, PAUSED, EXPIRED |
| planId | string | ID тарифного плана |
| startDate | string | Дата начала (ISO 8601) |
| endDate | string \| null | Дата окончания |
| isActive | boolean | Активна ли подписка |
| autoRenew | boolean | Автопродление |
| language | string | Язык |
| robokassaCode | string | Код Robokassa-конфигурации |
Лицензия
Проприетарный. Только для внутреннего использования.
