Node.js
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);
}
}
});
Обработка ошибок
Все методы могут бросать следующие исключения:
| Исключение | Когда |
|---|---|
ApiError | API вернул success=false или неожиданный HTTP-статус |
SignatureError | Не прошла верификация подписи сервера или вебхука |
PayinError | Базовый класс; перехватывает все ошибки SDK |
ApiError несёт поле errorCode (ErrorCode), вычисляемое из HTTP-статуса, и предикаты-хелперы:
| HTTP-статус | errorCode (ErrorCode) | Предикат | Когда |
|---|---|---|---|
| 400 | badRequest | isBadRequest() | Ошибка валидации запроса, в т.ч. не передан обязательный payerBank |
| 403 | forbidden | isForbidden() | Мерчант заблокирован/отключён |
| 404 | notFound | isNotFound() | Заказ не найден |
| 429 | rateLimited | isRateLimited() | Превышен лимит запросов |
| 5xx | serverError | isServerError() | Ошибка сервера |
| иное | 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 # Интеграционный тест