Payin H2H

Node.js

Node.js SDK для интеграции с Merchant Payment API.

Node.js

Node.js SDK для интеграции с Merchant Payment API.

Требования

  • Node.js >= 18.0.0

Установка

npm install github:unitewt/sdk-payin#main --install-links

Пакет подключается из подкаталога nodejs/, поэтому в вашем проекте добавьте в package.json:

{
    "dependencies": {
        "payin-sdk": "github:unitewt/sdk-payin#main"
    }
}

Или клонируйте репозиторий и подключите локально:

git clone https://github.com/unitewt/sdk-payin.git
npm install ./sdk-payin/nodejs

Генерация ключей ed25519

Перед началом работы необходимо сгенерировать пару ключей и передать публичный ключ в систему uniteplat:

const crypto = require('node:crypto');

const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');

const pubRaw = publicKey.export({ type: 'spki', format: 'der' }).slice(12);
const privRaw = privateKey.export({ type: 'pkcs8', format: 'der' }).slice(16);
const privFull = Buffer.concat([privRaw, pubRaw]);

console.log('Public key (передать в uniteplat):', pubRaw.toString('base64'));
console.log('Private key (хранить у себя):', privFull.toString('base64'));

Или используйте готовый скрипт:

npm run generate-keys

Быстрый старт

Инициализация клиента

const { Config, PayinClient } = require('payin-sdk');

const config = new Config({
  baseUrl: 'https://merchant.uniteplat.org',
  keyId: 'ваш-key-id',
  privateKey: Buffer.from('ваш-приватный-ключ-в-base64', 'base64'),
  serverPublicKey: Buffer.from('публичный-ключ-сервера-в-base64', 'base64'), // опционально, для верификации ответов
});

const client = new PayinClient(config);

Создание платежа (синхронный режим)

Реквизиты возвращаются сразу в ответе:

const { Currency, PaymentMethod } = require('payin-sdk');

const payment = await client.newPayment({
  sum: 100.50,
  currency: Currency.RUB,
  paymentMethod: PaymentMethod.CARD, // или PaymentMethod.SBP — обязательное поле
  customerAccount: 'user_123',
  statusWebhookUrl: 'https://your-site.com/webhook/status', // опционально
  payerBank: 'sberbank',                       // опционально (некоторые мерчанты требуют)
  redirectUrl: 'https://your-site.com/return', // опционально
  email: 'user@example.com',                   // опционально
  userCreatedAt: '2024-01-15T10:00:00Z',       // опционально, RFC3339
});

console.log(payment.orderId);          // ID заказа для дальнейших операций
console.log(payment.payeeAccount);     // Номер карты/счёта для оплаты
console.log(payment.payeeAccountBank); // Название банка (может быть null)
console.log(payment.payeeName);        // Имя получателя (может быть null)
console.log(payment.sum);              // Сумма к оплате
if (payment.deeplink) {
  console.log(payment.deeplink);       // Диплинк для оплаты (может быть null)
}

Поля payeeAccountBank, payeeName и deeplink могут быть null — учитывайте это при отображении.

Создание платежа (асинхронный режим)

Реквизиты придут на credentialsWebhookUrl после назначения. Ответ содержит только { success, orderId }:

const payment = await client.newPaymentAsync({
  sum: 100.50,
  currency: Currency.RUB,
  paymentMethod: PaymentMethod.SBP,
  customerAccount: 'user_123',
  credentialsWebhookUrl: 'https://your-site.com/webhook/credentials', // обязательно
  statusWebhookUrl: 'https://your-site.com/webhook/status',           // опционально
  redirectUrl: 'https://your-site.com/return',                       // опционально
  email: 'user@example.com',                                         // опционально
  userCreatedAt: '2024-01-15T10:00:00Z',                             // опционально
});

console.log(payment.orderId); // Показать пользователю страницу ожидания

credentialsWebhookUrl обязателен для асинхронного режима: реквизиты придут на этот URL после назначения.

Доступные суммы

Запрос подходящих к оплате сумм:

const result = await client.getAvailableAmounts({
  sum: 100.50,
  currency: Currency.RUB,
  paymentMethod: PaymentMethod.CARD,
  customerAccount: 'user_123', // опционально
  sumDriftUp: 5,               // опционально, допустимое отклонение вверх
  sumDriftDown: 5,             // опционально, допустимое отклонение вниз
});

console.log(result.availableAmounts); // массив чисел: [100.50, 105.00, ...]

Подтверждение оплаты

Вызывается после того, как пользователь сообщает, что совершил перевод:

const result = await client.confirm('abc123');
console.log(result.orderId);

Отмена платежа

const result = await client.cancel('abc123');

Отменить платёж можно, только пока он ожидает подтверждения оплаты пользователем либо находится на стадии арбитража (включая receiptRequired). Финальные статусы (doneSuccess, doneFail, arbitrageCancelledByMerchant) отменить нельзя.

Получение статуса

const { PaymentStatus, isDone, isArbitrageCancelledByMerchant } = require('payin-sdk');

const status = await client.getStatus('abc123');

console.log(status.status); // 'pending', 'doneSuccess', etc.

if (status.deeplink) {
  console.log(status.deeplink); // Ссылка-диплинк для оплаты (если настроена у мерчанта)
}

if (status.status === PaymentStatus.DONE_SUCCESS) {
  // платёж завершён успешно
}

if (status.status === PaymentStatus.RECEIPT_REQUIRED) {
  // необходимо прикрепить чек
}

if (isArbitrageCancelledByMerchant(status.status)) {
  // платёж отменён мерчантом в арбитраже — терминальный статус
}

if (isDone(status.status)) {
  // терминальный статус: doneSuccess, doneFail или arbitrageCancelledByMerchant
}

Возможные значения PaymentStatus:

ЗначениеОписание
PaymentStatus.FREE (free)Создан, ожидает назначения
PaymentStatus.PENDING (pending)Ожидает оплаты
PaymentStatus.RECEIPT_REQUIRED (receiptRequired)Требуется прикрепить чек
PaymentStatus.DONE_SUCCESS (doneSuccess)Завершён успешно (терминальный)
PaymentStatus.DONE_FAIL (doneFail)Завершён неуспешно (терминальный)
PaymentStatus.ARBITRAGE_CANCELLED_BY_MERCHANT (arbitrageCancelledByMerchant)Отменён мерчантом в арбитраже (терминальный)

Прикрепление чека

Требуется при статусе receiptRequired:

// Один файл
await client.attachReceipt('abc123', '/path/to/receipt.pdf');

// Несколько файлов
await client.attachReceipt('abc123', '/path/to/receipt1.pdf', '/path/to/receipt2.jpg');

Обработка вебхуков

Credentials webhook (credentialsWebhookUrl)

const { WebhookVerifier, SignatureError } = require('payin-sdk');

const verifier = new WebhookVerifier(Buffer.from('публичный-ключ-сервера-в-base64', 'base64'));

// В Express-хендлере (с raw body middleware):
app.post('/webhook/credentials', (req, res) => {
  try {
    const payload = verifier.verifyCredentialsWebhook(req.body, req.headers['x-signature']);

    console.log(payload.orderId);
    console.log(payload.payeeAccount);
    console.log(payload.payeeAccountBank);
    console.log(payload.payeeName);
    if (payload.deeplink) {
      console.log(payload.deeplink); // Ссылка-диплинк, если настроена у мерчанта
    }

    res.sendStatus(200);
  } catch (err) {
    if (err instanceof SignatureError) {
      res.sendStatus(403);
    }
  }
});

Status webhook (statusWebhookUrl)

const { WebhookVerifier, PaymentStatus, SignatureError } = require('payin-sdk');

const verifier = new WebhookVerifier(Buffer.from('публичный-ключ-сервера-в-base64', 'base64'));

app.post('/webhook/status', (req, res) => {
  try {
    const payload = verifier.verifyStatusWebhook(req.body, req.headers['x-signature']);

    switch (payload.status) {
      case PaymentStatus.DONE_SUCCESS:
        handleSuccess(payload.orderId);
        break;
      case PaymentStatus.DONE_FAIL:
        handleFailure(payload.orderId);
        break;
      case PaymentStatus.ARBITRAGE_CANCELLED_BY_MERCHANT:
        handleFailure(payload.orderId); // терминальный статус, как doneFail
        break;
      case PaymentStatus.RECEIPT_REQUIRED:
        handleReceiptRequired(payload.orderId);
        break;
    }

    res.sendStatus(200);
  } catch (err) {
    if (err instanceof SignatureError) {
      res.sendStatus(403);
    }
  }
});

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

Все методы могут бросать следующие исключения:

ИсключениеКогда
ApiErrorAPI вернул success=false или неожиданный HTTP-статус
SignatureErrorНе прошла верификация подписи сервера или вебхука
PayinErrorБазовый класс; перехватывает все ошибки SDK

ApiError несёт поле errorCode (ErrorCode), вычисляемое из HTTP-статуса, и предикаты-хелперы:

HTTP-статусerrorCode (ErrorCode)ПредикатКогда
400badRequestisBadRequest()Ошибка валидации запроса, в т.ч. не передан обязательный payerBank
403forbiddenisForbidden()Мерчант заблокирован/отключён
404notFoundisNotFound()Заказ не найден
429rateLimitedisRateLimited()Превышен лимит запросов
5xxserverErrorisServerError()Ошибка сервера
иноеunknownПрочее
const { ApiError, SignatureError, PayinError, ErrorCode } = require('payin-sdk');

try {
  const payment = await client.newPayment({
    sum: 100.50,
    currency: Currency.RUB,
    paymentMethod: PaymentMethod.CARD,
    customerAccount: 'user_123',
  });
} catch (err) {
  if (err instanceof ApiError) {
    // Ошибка API: err.message, err.orderId, err.statusCode, err.errorCode
    if (err.isRateLimited()) {
      // 429 — слишком много запросов, повторите позже
    } else if (err.isForbidden()) {
      // 403 — мерчант заблокирован или отключён
    } else if (err.isBadRequest()) {
      // 400 — ошибка валидации, в т.ч. не передан обязательный payerBank
    } else if (err.isNotFound()) {
      // 404 — заказ не найден
    }
  } else if (err instanceof SignatureError) {
    // Проблема с подписью
  } else if (err instanceof PayinError) {
    // Любая другая ошибка SDK
  }
}

Структура пакета

nodejs/
├── package.json
├── src/
│   ├── index.js     # Экспорты
│   ├── client.js    # Главный клиент
│   ├── config.js    # Параметры подключения
│   ├── signer.js    # Подпись/верификация ed25519
│   ├── http.js      # HTTP-клиент
│   ├── webhook.js   # Верификация вебхуков
│   └── errors.js    # Типы ошибок
└── examples/
    ├── generate_keys.js     # Генерация ключей
    └── integration_test.js  # Интеграционный тест
Copyright © 2026