@moneygatex/merchant-sdk
v1.3.0
Published
SDK for MoneyGateX merchant API: orders, webhook verification, NestJS module
Readme
@moneygatex/merchant-sdk
SDK для интеграции мерчантов с API MoneyGateX: создание заказов, получение статуса, проверка подписи webhook и готовый NestJS-модуль с обработчиком webhook.
Содержание
- Установка
- Быстрый старт
- Конфигурация NestJS
- API-клиент
- Привязка карты и автоплатёж
- Webhook
- Модуль NestJS
- Типы и точки входа
- Идемпотентность
- Ошибки и отладка
Установка
# Yarn
yarn add @moneygatex/merchant-sdk
# npm
npm install @moneygatex/merchant-sdkДля использования только NestJS-модуля в проекте должны быть установлены @nestjs/common и @nestjs/core (peer dependencies).
Подпути @moneygatex/merchant-sdk/client, .../webhook и .../nestjs объявлены в package.json и ведут на тот же артефакт, что и корень пакета — удобно для явного разделения импортов в коде.
Быстрый старт
Только API-клиент (Node.js или браузер)
import {
ECurrencyCode,
MerchantApiClient,
} from '@moneygatex/merchant-sdk/client';
const client = new MerchantApiClient({
baseUrl: 'https://api.example.com',
projectToken: 'sk_live_xxxx',
});
const order = await client.createOrder({
projectPublicId: 'ABC12XYZ',
amount: 1000.5,
currency: ECurrencyCode.USD,
merchantOrderId: 'ORDER-12345',
description: 'Payment for order #12345',
});
console.log(order.paymentUrl); // Ссылка на страницу оплаты (может быть на домене проекта или PayGate X — используйте как есть)
const rebill = await client.rebill(order.publicId, {
amount: 1000.5,
options: { recurring: 1 },
});
console.log(rebill.orders?.[0]?.status);NestJS: модуль + сервис + свой webhook-эндпоинт
// app.module.ts
import { MerchantSdkModule } from '@moneygatex/merchant-sdk/nestjs';
import { Module } from '@nestjs/common';
import { PaymentsController } from './payments.controller';
@Module({
imports: [
MerchantSdkModule.forRoot({
baseUrl: process.env.MONEYGATEX_API_URL!,
projectToken: process.env.MONEYGATEX_PROJECT_TOKEN!,
projectPublicId: process.env.MONEYGATEX_PROJECT_PUBLIC_ID,
webhookSecret: process.env.MONEYGATEX_WEBHOOK_SECRET, // для MerchantWebhookGuard
}),
],
controllers: [PaymentsController],
})
export class AppModule {}// payments.controller.ts — свой контроллер и эндпоинт для webhook
import {
MerchantWebhookBody,
MerchantWebhookGuard,
} from '@moneygatex/merchant-sdk/nestjs';
import type { WebhookEvent } from '@moneygatex/merchant-sdk/webhook';
import { Controller, Post, UseGuards } from '@nestjs/common';
@Controller('payments')
export class PaymentsController {
@Post('webhook')
@UseGuards(MerchantWebhookGuard)
handleWebhook(@MerchantWebhookBody() event: WebhookEvent): string {
if (event.event === 'order.charged') {
console.log(
'Order charged:',
event.data.publicId,
event.data.amountCharged,
);
// Обновить заказ в БД, отправить уведомление и т.д.
}
return 'OK'; // обязательно 200 и тело "OK"
}
}// orders.controller.ts — projectPublicId можно не передавать, если задан в опциях модуля
import { ECurrencyCode } from '@moneygatex/merchant-sdk/client';
import { MerchantSdkService } from '@moneygatex/merchant-sdk/nestjs';
import { Controller, Param, Post } from '@nestjs/common';
@Controller('orders')
export class OrdersController {
constructor(private readonly merchantSdk: MerchantSdkService) {}
@Post()
async create() {
return this.merchantSdk.createOrder({
amount: 99.99,
currency: ECurrencyCode.EUR,
});
}
@Post(':publicId/rebill')
async rebill(@Param('publicId') publicId: string) {
return this.merchantSdk.rebill(publicId, {
amount: 99.99,
options: { recurring: 1 },
});
}
}Конфигурация NestJS
forRoot(options)
Синхронная конфигурация при старте приложения.
| Параметр | Тип | Обязательный | Описание |
| ----------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| baseUrl | string | да | Базовый URL API (например https://api.example.com) |
| projectToken | string | да | Токен проекта (Bearer) |
| projectPublicId | string | нет | Публичный ID проекта; подставляется в createOrder, startCardAuth, credit, listCardFingerprints, если не передан в вызове |
| webhookSecret | string | нет | Секрет для проверки подписи webhook (нужен для MerchantWebhookGuard) |
forRootAsync(options)
Асинхронная конфигурация (например из ConfigService).
| Параметр | Тип | Описание |
| ------------ | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| useFactory | (...args) => MerchantSdkModuleOptions \| Promise<MerchantSdkModuleOptions> | Функция, возвращающая опции (можно async) |
| inject | (InjectionToken \| OptionalFactoryDependency)[] | Массив токенов для инъекции аргументов в useFactory (например [ConfigService]) |
Пример с кастомным ConfigModule:
// app.module.ts
import { MerchantSdkModule } from '@moneygatex/merchant-sdk/nestjs';
import { Module } from '@nestjs/common';
import { CustomConfigModule } from '@/config/config.module';
import { CustomConfigService } from '@/config/config.service';
@Module({
imports: [
MerchantSdkModule.forRootAsync({
imports: [CustomConfigModule],
inject: [CustomConfigService],
useFactory: (configService: CustomConfigService) => ({
baseUrl: configService.merchantSdkBaseUrl,
projectToken: configService.merchantSdkProjectToken,
projectPublicId: configService.merchantSdkProjectPublicId,
webhookSecret: configService.merchantSdkWebhookSecret,
}),
}),
],
})
export class AppModule {}В CustomConfigService добавьте геттеры (значения из env или валидированного конфига), например:
get merchantSdkBaseUrl(): string {
return this.config.MONEYGATEX_API_URL ?? '';
}
get merchantSdkProjectToken(): string {
return this.config.MONEYGATEX_PROJECT_TOKEN ?? '';
}
get merchantSdkProjectPublicId(): string | undefined {
return this.config.MONEYGATEX_PROJECT_PUBLIC_ID;
}
get merchantSdkWebhookSecret(): string | undefined {
return this.config.MONEYGATEX_WEBHOOK_SECRET;
}API-клиент
Точка входа: @moneygatex/merchant-sdk/client.
Конфигурация
import { MerchantApiClient } from '@moneygatex/merchant-sdk/client';
const client = new MerchantApiClient({
baseUrl: 'https://api.example.com', // без завершающего /
projectToken: 'sk_live_xxxx',
});createOrder(params, options?)
Создаёт заказ. Требуется токен проекта.
Параметры (CreateOrderParams):
| Поле | Тип | Обязательный | Описание |
| --------------------- | --------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------- |
| projectPublicId | string | да | Публичный ID проекта (до 50 символов) |
| amount | number | да | Сумма (0.01–999999.99, до 2 знаков после запятой) |
| currency | ECurrencyCode | да | EUR | USD | GBP | AUD | RUB | BYN |
| merchantOrderId | string \| null | нет | ID заказа мерчанта (до 255 символов) |
| description | string \| null | нет | Описание заказа |
| metadata | Record<string, unknown> \| null | нет | Произвольный JSON |
| projectUserId | string | нет | Вместе с cardFingerprintUuid — автосписание (rebill в фоне), без обязательного редиректа на оплату |
| cardFingerprintUuid | string (UUID) | нет | Вместе с projectUserId — автосписание по сохранённой карте |
Опции: { idempotencyKey?: string } — ключ идемпотентности (см. Идемпотентность).
Возвращает: Promise<CreateOrderResponse> — publicId, amount, currency, status, expiresAt, paymentUrl, merchantOrderId, description, metadata, опционально autoPay.
- Без автоплатежа (поля
projectUserId/cardFingerprintUuidне переданы или передано только одно из них): ведите пользователя наpaymentUrlдля оплаты; итог — webhook и/илиgetStatus(publicId). - С автоплатежом (оба поля заданы): в ответе появляется
autoPay: { pending: true, state: 'queued' }— rebill ставится в очередь на бэкенде; итог — webhook и/илиgetStatus. ПолеpaymentUrlвсё равно присутствует, но обычно редирект на страницу оплаты не нужен.
Примечание по paymentUrl: ссылка на страницу оплаты (order.paymentUrl) может вести на домен проекта (например https://pay.merchant.com/pay/...), если в настройках проекта указан «Базовый URL страницы оплаты», иначе — на домен PayGate X. Всегда используйте возвращённый paymentUrl как есть, не полагайтесь на фиксированный домен.
startCardAuth(params)
Старт привязки карты под пользователем проекта (POST /v1/card-auth/start): создаётся заказ на фиксированную сумму, пользователь переходит по paymentUrl. Требуется токен проекта.
Параметры: projectPublicId, projectUserId (обязательны), опционально currency, pan, card, client, custom_fields, returnUrl — см. тип StartCardAuthParams.
Возвращает: Promise<StartCardAuthResponse> — paymentUrl, publicId, опционально sessionId.
listCardFingerprints(projectUserId, projectPublicId)
Список активных отпечатков карт (GET /v1/card-fingerprints/:projectUserId?projectPublicId=...). Требуется токен проекта.
Возвращает: Promise<CardFingerprintListResponse> — projectUserId, cards[] с полями uuid, currency, isDefault, cardType?, panMask?, expirationMonth?, expirationYear?.
getOrder(publicId)
Получает заказ по публичному ID. Публичный эндпоинт, токен не используется.
Возвращает: Promise<OrderPublicResponse> — данные заказа и merchantName.
getStatus(publicId)
Получает статус заказа по публичному ID. Публичный эндпоинт.
Возвращает: Promise<OrderStatusResponse> — status, publicId, providerOrderId?.
rebill(publicId, params)
Повторное списание по заказу. Требуется токен проекта.
Параметры:
publicId: string— публичный ID заказа.params: RebillParams— обязательно:amount: number— сумма списания (требуется провайдером).options?: { recurring?: 0 | 1 }
Возвращает: Promise<ProviderOrderResponse>.
Пример:
const rebill = await client.rebill('ORDER_PUBLIC_ID', {
amount: 100.5,
options: { recurring: 1 },
});credit(params)
Перевод на карту (OCT, POST /v1/orders/credit). Требуется токен проекта.
Параметры — union CreditParams:
- Полные реквизиты карты и клиента:
projectPublicId,amount,currency,pan,card,client,custom_fields, опциональноmerchant_order_id. - Сохранённая карта:
projectPublicId,amount,currency,projectUserId,cardFingerprintUuid(остальное подставляет бэкенд из отпечатка и данных привязки).
Возвращает: Promise<CreateOrderResponse> — тот же формат, что после createOrder (publicId, status, paymentUrl, expiresAt и т.д.). Поле autoPay в ответе OCT не используется (итог операции смотрите по status и webhook).
Миграция с 1.1.x: раньше тип ответа был ProviderOrderResponse (сырой ответ провайдера). Теперь используйте поля заказа (publicId, status, …), а не result.orders?.[0].
Пример с полной картой:
const result = await client.credit({
projectPublicId: 'ABC12XYZ',
amount: 99.99,
currency: ECurrencyCode.EUR,
pan: '4111111111111111',
card: {
expiration_month: 12,
expiration_year: 2030,
holder: 'JOHN SMITH',
},
client: {
name: 'JOHN SMITH',
country: 'USA',
},
custom_fields: {
recipient_birth_date: '1999-12-16',
recipient_first_name: 'JOHN',
recipient_last_name: 'SMITH',
},
});Пример по сохранённому отпечатку:
const result = await client.credit({
projectPublicId: 'ABC12XYZ',
amount: 99.99,
currency: ECurrencyCode.EUR,
projectUserId: 'user-42',
cardFingerprintUuid: '550e8400-e29b-41d4-a716-446655440000',
});cancel(publicId, params)
Отмена заказа: при статусе charged у провайдера выполняется refund, при authorized — reverse. Требуется токен проекта.
Параметры (CancelParams):
amount: number— обязательно. Сумма возврата/отмены.
Возвращает: Promise<ProviderOrderResponse>.
Пример:
const result = await client.cancel('ORDER_PUBLIC_ID', { amount: 50.0 });
console.log(result.orders?.[0]?.status);Привязка карты и автоплатёж
Типичный сценарий:
startCardAuth— получитьpaymentUrl, открыть пользователю оплату фиксированной суммы для привязки.- После успешной оплаты — отпечаток карты доступен; при необходимости
listCardFingerprintsдля выбора карты поuuid. createOrderсprojectUserIdиcardFingerprintUuid— автосписание без редиректа: в ответе будетautoPay; дождитесьorder.charged/order.status_changedили опроситеgetStatus(publicId).
Без пары projectUserId + cardFingerprintUuid заказ создаётся как обычно — редирект на paymentUrl.
Webhook
Точка входа: @moneygatex/merchant-sdk/webhook.
MoneyGateX отправляет webhook POST-запросом на URL, указанный в настройках проекта. Обязательно возвращайте статус 200 и тело OK (текст/plain).
Заголовки и подпись
- X-Webhook-Timestamp — Unix-время (секунды).
- X-Webhook-Signature — подпись в формате
v1,{hex}.
Подпись вычисляется так:
HMAC-SHA256(timestamp + "." + rawBody, webhookSecret)Строка для подписи: {timestamp}.{rawBody} (сырое тело запроса без изменений). Результат — hex (64 символа).
verifyWebhookSignature
Проверка подписи вручную (Express, Koa, другой фреймворк):
import { verifyWebhookSignature } from '@moneygatex/merchant-sdk/webhook';
// rawBody — строка: сырое тело запроса (не парсенный JSON)
const isValid = verifyWebhookSignature(
req.headers['x-webhook-timestamp'],
req.headers['x-webhook-signature'],
rawBody,
process.env.WEBHOOK_SECRET!,
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
if (event.event === 'order.charged') {
// Обработка
}
res.status(200).send('OK');Типы событий
order.charged — успешное списание по заказу.
import type {
OrderChargedEvent,
OrderChargedEventData,
} from '@moneygatex/merchant-sdk/webhook';
// OrderChargedEvent
{
event: 'order.charged';
data: {
orderId: string;
publicId: string;
amount: number;
amountCharged: number;
currency: string;
status: 'SUCCEEDED'; // в SDK: EOrderChargeStatus
chargedAt: string; // ISO 8601
}
}order.status_changed — изменение статуса заказа (отмена/refund/reverse по API, перевод на карту — credit, истечение срока и т.д.). В панели управления в настройках Webhook можно отправить имитацию этого события — в data будет передан simulated: true.
import type { OrderStatusChangedEvent, OrderStatusChangedEventData } from '@moneygatex/merchant-sdk/webhook';
// event.event === 'order.status_changed'
{
event: 'order.status_changed';
data: {
orderId: string;
publicId: string;
amount: number;
currency: string;
status: string; // например 'CANCELED', 'REFUNDED'
changedAt: string; // ISO 8601
reason: string | null; // 'expired', 'refund', 'reverse', 'credit' и др.
metadata?: Record<string, unknown> | null; // опционально, передаётся при наличии
simulated?: true; // присутствует только при имитации из настроек
};
}Типы для этого события: OrderStatusChangedEvent, OrderStatusChangedEventData. Экспортируются также: OrderChargedEvent, OrderChargedEventData, WebhookEvent, EWebhookEvent.
Модуль NestJS
Точка входа: @moneygatex/merchant-sdk/nestjs.
Что даёт модуль
- MerchantSdkService — инжектируемый сервис:
createOrder,startCardAuth,listCardFingerprints,getOrder,getStatus,rebill,credit,cancel. ДляprojectPublicIdв теле запросов действует та же подстановка из опций модуля, что и дляcreateOrder. - MerchantWebhookGuard — guard для проверки подписи webhook. Вешается на ваш эндпоинт через
@UseGuards(MerchantWebhookGuard). - MerchantWebhookBody — параметр-декоратор: извлекает и парсит тело webhook в типизированный
WebhookEvent. Использовать в обработчике:handleWebhook(@MerchantWebhookBody() event: WebhookEvent).
Контроллер и путь эндпоинта вы создаёте сами; SDK только проверяет подпись и отдаёт распарсенные данные.
Raw body для webhook
Проверка подписи требует сырого тела запроса. В NestJS достаточно создать приложение с опцией rawBody: true и не подключать глобально express.json() — встроенный парсер Nest сохранит сырое тело в req.rawBody.
В main.ts:
import { NestFactory } from '@nestjs/core';
import * as express from 'express';
import { AppModule } from './app.module';
async function bootstrap() {
// rawBody: true — Nest сохраняет сырое тело в req.rawBody (нужно для проверки подписи webhook)
const app = await NestFactory.create(AppModule, { rawBody: true });
// express.json() не подключать — иначе сырое тело будет потреблено и guard вернёт 401
app.use(express.urlencoded({ extended: true }));
// ...
await app.listen(3000);
}Если не включить rawBody: true или подключить app.use(express.json()), guard вернёт 401 «Missing raw body for webhook verification».
Экспорты из nestjs
MerchantSdkModule— модуль.MerchantSdkService— сервис API.MerchantWebhookGuard— guard проверки подписи webhook.MerchantWebhookBody— декоратор параметра для тела webhook.MerchantSdkModuleOptions— тип опций.- Типы с опциональным
projectPublicId:CreateOrderParamsWithOptionalProjectId,StartCardAuthParamsWithOptionalProjectId,CreditParamsWithOptionalProjectId.
Типы и точки входа
| Подключение | Содержимое |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| @moneygatex/merchant-sdk | Клиент + webhook (без NestJS) |
| @moneygatex/merchant-sdk/client | MerchantApiClient, MerchantApiError (поле body — разобранное тело ошибки API), типы заказов и OCT (CreditParams*, StartCardAuth*, CardFingerprint*, …), ECurrencyCode, EOrderStatus, MerchantApiErrorCode |
| @moneygatex/merchant-sdk/webhook | verifyWebhookSignature, WebhookEvent, OrderChargedEvent, OrderChargedEventData, OrderStatusChangedEvent, OrderStatusChangedEventData, EWebhookEvent, EOrderChargeStatus |
| @moneygatex/merchant-sdk/nestjs | MerchantSdkModule, MerchantSdkService, опциональные типы параметров с projectPublicId, MerchantWebhookGuard, MerchantWebhookBody, MerchantSdkModuleOptions |
Типы API: CreateOrderParams, CreateOrderResponse, MerchantOrderResponse, StartCardAuthParams, StartCardAuthResponse, CardFingerprintListResponse, OrderPublicResponse, OrderStatusResponse, RebillParams, CreditParams, CancelParams, ProviderOrderResponse (rebill/cancel).
Идемпотентность
Бэкенд поддерживает идемпотентность создания заказа через заголовок Idempotency-Key. При повторной отправке того же ключа в течение срока действия возвращается сохранённый ответ.
await client.createOrder(params, { idempotencyKey: 'unique-key-123' });В NestJS:
await this.merchantSdk.createOrder(params, {
idempotencyKey: 'unique-key-123',
});Используйте уникальный ключ на операцию (например UUID или order-{merchantOrderId}).
Ошибки и отладка
MerchantApiError
При ошибках HTTP API клиент выбрасывает MerchantApiError (из @moneygatex/merchant-sdk/client):
import {
MerchantApiClient,
MerchantApiError,
} from '@moneygatex/merchant-sdk/client';
try {
await client.createOrder(params);
} catch (err) {
if (err instanceof MerchantApiError) {
console.error(err.statusCode, err.message);
}
throw err;
}- 401 — неверный или отсутствующий токен проекта.
- 4xx/5xx — тело ответа API передаётся в
message; при JSON в формате API также доступноerr.body(MerchantApiErrorBody).
Webhook
- Invalid signature (401) — неверный
webhookSecret, изменённое тело запроса или не тот raw body (например парсенный JSON вместо строки). - Убедитесь, что ответ на webhook: статус 200, тело строка
OK.
Рекомендации
- Храните
baseUrl,projectTokenиwebhookSecretв переменных окружения или секретах. - В логах не выводите токен и секрет целиком.
- Для отладки webhook можно логировать заголовки (без подписи) и длину тела.
Лицензия
UNLICENSED (проприетарный пакет).
