npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-конфигурации |

Лицензия

Проприетарный. Только для внутреннего использования.