Создание платежа
POST https://api.meridian.vip/api/v1/transgran/payments
Обязательные параметры
| Параметр | Тип | Описание |
|---|---|---|
amount | number | Сумма платежа в рублях (не в копейках). Пример: 1000 = 1000 RUB |
currency | string | Код валюты. Только "RUB" |
bank | number | Предпочтительный банк клиента. Система подберет реквизиты с учетом этого предпочтения. См. Коды банков |
internalId | string | Уникальный идентификатор заказа в вашей системе для идемпотентности |
customerEmail | string | Email клиента (требуется для Трансгран) |
notificationUrl | string | URL для webhook уведомлений по этому платежу |
notificationToken | string | Секретный токен для подписи webhook уведомлений , минимальная длина 32 символа |
Коды банков
Каждый код банка — это одновременно и банк плательщика, и провайдер, который обслуживает платёж. Коды1–6 и 7–12 — это одни и те же шесть банков у двух разных провайдеров. Один и тот же
мерчант может одновременно работать с обоими: балансы, выплаты и реквизиты у них раздельные.
| Код | Банк | Провайдер |
|---|---|---|
1 | VTB | основной |
2 | Sberbank | основной |
3 | Gazprombank | основной |
4 | T-Bank | основной |
5 | Solidarnost | основной |
6 | Alfabank | основной |
7 | VTB | BankApi |
8 | Sberbank | BankApi |
9 | Gazprombank | BankApi |
10 | T-Bank | BankApi |
11 | Solidarnost | BankApi |
12 | Alfabank | BankApi |
13 | СБП QR | BankApi |
Коды
7–13 доступны только после подключения провайдера BankApi к вашему аккаунту. Запрос с
этими кодами без подключения вернёт 400.Для кодов
7–12 поддерживается только выдача карты: paymentMethod: "TO_PHONE" вернёт 400,
а в ответе всегда заполнен cardNumber. Поле phoneNumber при этом остаётся в ответе со значением
null (если paymentMethod не передан) — не рассчитывайте на его отсутствие, проверяйте значение.Код 13 — СБП QR
Код 13 — это не банк, а платёжная ссылка СБП. Плательщик открывает приложение любого своего
банка и сканирует QR-код; банк отправителя заранее не известен и не выбирается.
Отличия от всех остальных кодов:
- Параметр
paymentMethodне поддерживается. НиTO_CARD, ниTO_PHONE— оба значения вернут400. Просто не передавайте этот параметр. - В
dealRequisitesприходитurlвместоcardNumber. ПолейcardNumberиphoneNumberв ответе нет вообще — платёжным инструментом является сама ссылка. Отрисуйте её как QR-код или передайте плательщику ссылкой. bankNameв ответе —bankapi_sbp_qr.
{
"bankName": "bankapi_sbp_qr",
"amount": "1050.00",
"dealRequisites": "{\"bankName\":\"bankapi_other\",\"fullName\":\"ООО Ромашка\",\"url\":\"https://qr.nspk.ru/AS10...\"}"
}
Сумма в ответе может отличаться от запрошенной. Для кода
13 банк-получатель выставляет счёт
на скорректированную сумму — например, вы запросили 1000, а в ответе пришло 1050. Так
разделяются платежи на один общий QR-реквизит.Определяющей является сумма из ответа, поле amount. Именно её должен перевести плательщик,
и именно по ней вы будете рассчитаны. Если вы покажете клиенту свою исходную сумму и он переведёт
её, платёж не будет засчитан автоматически и потребует ручного разбора.Всегда берите сумму для показа плательщику из поля amount ответа, а не из своего запроса. Это
же значение приходит во всех вебхуках по платежу.Неизвестный код банка возвращает
400 со списком допустимых значений. Ранее такой запрос мог быть
молча обработан основным провайдером — теперь он всегда отклоняется.Пример запроса
const crypto = require('crypto');
function calculateSignature(method, url, body, secret) {
const stringToSign = method + url + (body || '');
const hmac = crypto.createHmac('sha256', secret);
hmac.update(stringToSign);
return hmac.digest('base64');
}
const method = 'POST';
const url = 'https://api.meridian.vip/api/v1/transgran/payments';
const body = JSON.stringify({
amount: 1000,
currency: 'RUB',
bank: 2, // Sberbank
internalId: 'order-12345',
customerEmail: '[email protected]',
notificationUrl: 'https://your-site.com/webhooks/transgran',
notificationToken: 'your-webhook-secret-token'
});
const apiKey = 'meridian_abc123...:meridian_xyz789...';
const [keyId, secret] = apiKey.split(':');
const signature = calculateSignature(method, url, body, secret);
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Signature': signature
},
body
});
const result = await response.json();
console.log(result);
import hmac
import hashlib
import base64
import requests
import json
def calculate_signature(method, url, body, secret):
string_to_sign = method + url + (body or '')
signature = hmac.new(
secret.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode('utf-8')
method = 'POST'
url = 'https://api.meridian.vip/api/v1/transgran/payments'
body = json.dumps({
'amount': 1000,
'currency': 'RUB',
'bank': 2, # Sberbank
'internalId': 'order-12345',
'customerEmail': '[email protected]',
'notificationUrl': 'https://your-site.com/webhooks/transgran',
'notificationToken': 'your-webhook-secret-token'
})
api_key = 'luma_abc123...:luma_xyz789...'
key_id, secret = api_key.split(':')
signature = calculate_signature(method, url, body, secret)
response = requests.post(
url,
headers={
'Content-Type': 'application/json',
'X-API-Key': api_key,
'X-Signature': signature
},
data=body
)
result = response.json()
print(result)
<?php
function calculateSignature($method, $url, $body, $secret) {
$stringToSign = $method . $url . ($body ?? '');
$signature = hash_hmac('sha256', $stringToSign, $secret, true);
return base64_encode($signature);
}
$method = 'POST';
$url = 'https://api.meridian.vip/api/v1/transgran/payments';
$body = json_encode([
'amount' => 1000,
'currency' => 'RUB',
'bank' => 2, // Sberbank
'internalId' => 'order-12345',
'customerEmail' => '[email protected]',
'notificationUrl' => 'https://your-site.com/webhooks/transgran',
'notificationToken' => 'your-webhook-secret-token'
]);
$apiKey = 'luma_abc123...:luma_xyz789...';
list($keyId, $secret) = explode(':', $apiKey);
$signature = calculateSignature($method, $url, $body, $secret);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
'X-Signature: ' . $signature
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
print_r($result);
?>
Пример ответа (успех)
HTTP Status: 200 OK{
"id": "cm3k8x7y80001z8j4k5m6n7o8",
"status": "new",
"bankName": "sberbank",
"amount": "1000",
"currency": "RUB",
"expireAt": "2025-11-03T15:10:00.000Z",
"dealRequisites": "{\"bankName\":\"sberbank\",\"cardNumber\":\"**** 1234\",\"fullName\":\"Ivan Ivanov\"}",
"dealRate": "95.50",
"createdAt": "2025-11-03T15:00:00.000Z",
"updatedAt": "2025-11-03T15:00:00.000Z",
"internalId": "order-12345",
"merchantName": "MerchantName",
"clientEmail": "[email protected]"
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
id | string | Уникальный идентификатор платежа в системе Meridian |
status | string | Статус: new, paid, expired, canceled |
bankName | string | Запрошенный банк из параметра bank (sberbank, vtb, tbank, gazprombank, solidarnost, см. коды банков выше). Не является банком назначения — см. dealRequisites |
amount | string | Сумма платежа |
currency | string | Валюта (RUB) |
expireAt | string | Время истечения блокировки реквизита. ISO 8601 |
dealRequisites | string | JSON-строка с реквизитами для оплаты. Это фактический банк назначения. См. Структура dealRequisites |
dealRate | string | Курс сделки |
createdAt | string | Время создания платежа. ISO 8601 |
updatedAt | string | Время последнего обновления. ISO 8601 |
internalId | string | Ваш идентификатор заказа |
merchantName | string | Название мерчанта |
clientEmail | string | Email клиента |
Структура dealRequisites
ПолеdealRequisites содержит JSON-строку с реквизитами, куда клиент должен отправить платеж.
Важно: Поле
bankName в ответе — это запрошенный российский банк. Фактический банк назначения находится в dealRequisites.bankName и является одним из узбекских банков-партнеров.Поля объекта
| Поле | Тип | Описание |
|---|---|---|
bankName | string | Банк назначения: poytaxt_bank, garant_bank, asia_alliance, или ‘octobank’ |
cardNumber | string | Номер карты |
phoneNumber | string | Номер телефона |
fullName | string | ФИО получателя |
url | string | Платёжная ссылка СБП. Только для банка 13 — там она заменяет cardNumber и phoneNumber, которых в ответе нет. См. Код 13 — СБП QR |
Пример парсинга
const payment = await response.json();
const requisites = JSON.parse(payment.dealRequisites);
// bankName в ответе = "sberbank" (запрошенный банк)
// requisites.bankName = "garant_bank" (фактический банк назначения)
console.log('Банк назначения:', requisites.bankName); // "garant_bank"
console.log('Номер карты:', requisites.cardNumber); // "1234 5678 8765 4321"
console.log('Получатель:', requisites.fullName); // "Ivan Ivanov"
Ошибки создания платежа
При создании платежа могут возникнуть следующие ошибки:| HTTP код | Сообщение | Описание |
|---|---|---|
400 | Сумма X RUB выходит за пределы лимитов мерчанта | Сумма платежа выходит за установленные лимиты мерчанта. Обратитесь к администратору для уточнения лимитов |
400 | amount must be a positive number | Сумма должна быть положительным числом |
400 | currency must be RUB | Валюта должна быть RUB |
400 | bank must be one of: 1 (VTB), 2 (Sberbank), … , 13 (СБП QR (BankApi)) | Неверный код банка. В сообщении перечисляются все допустимые коды 1–13 с названиями банков |
400 | paymentMethod TO_CARD is not supported for bank 13 | Банк 13 (СБП QR) не поддерживает выбор типа реквизита — не передавайте paymentMethod |
400 | internalId is required | Не указан идентификатор заказа |
400 | customerEmail must be a valid email | Неверный формат email |
400 | notificationUrl must be a valid URL | Неверный URL для webhook |
400 | Transgran service not configured | Сервис временно недоступен |
400 | No requisites in response | Нет доступных реквизитов для выбранного банка. Попробуйте другой банк |
403 | API key authentication required | Требуется API-ключ аутентификации |
Пример ответа с ошибкой
HTTP Status: 400 Bad Request{
"error": "Сумма 50000 RUB выходит за пределы лимитов мерчанта"
}
Проверка статуса
Endpoint для проверки статуса платежа:GET https://api.meridian.vip/api/v1/transgran/payments/:id
Пример запроса
const method = 'GET';
const paymentId = 'cm3k8x7y80001z8j4k5m6n7o8';
const url = `https://api.meridian.vip/api/v1/transgran/payments/${paymentId}`;
const signature = calculateSignature(method, url, '', secret);
const response = await fetch(url, {
method,
headers: {
'X-API-Key': apiKey,
'X-Signature': signature
}
});
const payment = await response.json();
console.log('Status:', payment.status);
Пример ответа
{
"id": "cm3k8x7y80001z8j4k5m6n7o8",
"status": "paid",
"bankName": "sberbank",
"amount": "1000",
"currency": "RUB",
"expireAt": "2025-11-03T15:10:00.000Z",
"dealRequisites": "{\"bankName\":\"garant_bank\",\"cardNumber\":\"**** 1234\",\"fullName\":\"Ivan Ivanov\"}",
"dealRate": "95.50",
"createdAt": "2025-11-03T15:00:00.000Z",
"updatedAt": "2025-11-03T15:05:00.000Z",
"internalId": "order-12345",
"merchantName": "MerchantName",
"clientEmail": "[email protected]"
}
Webhook-уведомления
При изменении статуса платежа или разрешении апелляции система отправляет webhook на указанныйnotificationUrl.
HTTP запрос
POST {notificationUrl}
Content-Type: application/json
X-Webhook-Signature: {hmac_sha256_signature}
X-Webhook-Event: {event_type}
X-Webhook-Delivery-Id: {unique_delivery_id}
Формат тела webhook
{
"id": "cm3k8x7y80001z8j4k5m6n7o8",
"status": "paid",
"bankName": "sberbank",
"amount": "1000",
"currency": "RUB",
"expireAt": "2025-11-03T15:10:00.000Z",
"dealRequisites": "{\"bankName\":\"garant_bank\",\"cardNumber\":\"**** 1234\",\"fullName\":\"Ivan Ivanov\"}",
"dealRate": "95.50",
"createdAt": "2025-11-03T15:00:00.000Z",
"updatedAt": "2025-11-03T15:05:00.000Z",
"internalId": "order-12345",
"merchantName": "MerchantName",
"clientEmail": "[email protected]"
}
Поля webhook
| Поле | Тип | Описание |
|---|---|---|
id | string | ID платежа в Meridian |
status | string | Новый статус платежа |
bankName | string | Запрошенный российский банк (не банк назначения) |
amount | string | Сумма платежа |
currency | string | Валюта |
expireAt | string | Время истечения. ISO 8601 |
dealRequisites | string | JSON-строка с реквизитами. См. Структура dealRequisites |
dealRate | string | Курс сделки |
createdAt | string | Время создания платежа |
updatedAt | string | Время последнего обновления |
internalId | string | Ваш идентификатор заказа |
merchantName | string | Название мерчанта |
clientEmail | string | Email клиента |
Верификация подписи
Webhook подписывается HMAC-SHA256. Используйте вашnotificationToken для верификации:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
Создание апелляции
Если автоматическая привязка платежа не сработала (клиент оплатил, но статус не изменился), создайте апелляцию для ручной проверки.Когда создавать апелляцию: Клиент утверждает, что оплатил, но платеж остается в статусе
new или expired.Важно: После создания апелляции платеж переходит в статус
dispute, и все средства замораживаются.Endpoint
POST https://api.meridian.vip/api/v1/transgran/payments/:id/disputes
Параметры запроса
URL параметры
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
id | string | Да | Уникальный идентификатор платежа. Пример: cm3k8x7y80001z8j4k5m6n7o8 |
Тело запроса (multipart/form-data)
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
reason | string | Да | Причина спора. См. допустимые значения ниже |
description | string | Нет | Подробное описание проблемы (макс. 5000 символов) |
disputeReasonData | string | Нет | JSON-строка. Обязательно для reason="invalid_sum": {"amount": number} |
attachment | file | Нет | Файл-доказательство (скриншот, чек). Макс. 10 МБ. Форматы: JPG, PNG, PDF |
reason:
| Значение | Описание | Ограничения |
|---|---|---|
invalid_sum | Неверная сумма платежа | Требует disputeReasonData.amount (фактическая сумма) |
has_payment | Платеж был совершен, но не засчитан | - |
invalid_requisites | Некорректные реквизиты | - |
unknown | Неясная причина спора | - |
Пример запроса
METHOD="POST"
PAYMENT_ID="cm3k8x7y80001z8j4k5m6n7o8"
URL="https://api.meridian.vip/api/v1/transgran/payments/${PAYMENT_ID}/disputes"
JSON_DATA='{"reason":"invalid_sum","description":"Клиент отправил 500 RUB","disputeReasonData":{"amount":500}}'
SIGNATURE=$(echo -n "${METHOD}${URL}${JSON_DATA}" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
curl -X POST "${URL}" \
-H "X-API-Key: ${API_KEY}" \
-H "X-Signature: ${SIGNATURE}" \
-F "reason=invalid_sum" \
-F "description=Клиент отправил 500 RUB вместо 1000 RUB" \
-F 'disputeReasonData={"amount":500}' \
-F "attachment=@/path/to/screenshot.jpg"
const crypto = require('crypto');
const FormData = require('form-data');
const fs = require('fs');
function calculateSignature(method, url, body, secret) {
const stringToSign = method + url + (body || '');
return crypto.createHmac('sha256', secret).update(stringToSign).digest('base64');
}
const paymentId = 'cm3k8x7y80001z8j4k5m6n7o8';
const method = 'POST';
const url = `https://api.meridian.vip/api/v1/transgran/payments/${paymentId}/disputes`;
const jsonData = JSON.stringify({
reason: 'invalid_sum',
description: 'Клиент отправил 500 RUB вместо 1000 RUB',
disputeReasonData: { amount: 500 }
});
const apiKey = 'meridian_abc123...:meridian_xyz789...';
const [keyId, secret] = apiKey.split(':');
const signature = calculateSignature(method, url, jsonData, secret);
const formData = new FormData();
formData.append('reason', 'invalid_sum');
formData.append('description', 'Клиент отправил 500 RUB вместо 1000 RUB');
formData.append('disputeReasonData', JSON.stringify({ amount: 500 }));
formData.append('attachment', fs.createReadStream('/path/to/screenshot.jpg'));
const response = await fetch(url, {
method,
headers: {
...formData.getHeaders(),
'X-API-Key': apiKey,
'X-Signature': signature
},
body: formData
});
const result = await response.json();
console.log(result);
import hmac
import hashlib
import base64
import requests
import json
def calculate_signature(method, url, body, secret):
string_to_sign = method + url + (body or '')
return base64.b64encode(
hmac.new(secret.encode(), string_to_sign.encode(), hashlib.sha256).digest()
).decode()
payment_id = 'cm3k8x7y80001z8j4k5m6n7o8'
method = 'POST'
url = f'https://api.meridian.vip/api/v1/transgran/payments/{payment_id}/disputes'
json_data = json.dumps({
'reason': 'invalid_sum',
'description': 'Клиент отправил 500 RUB вместо 1000 RUB',
'disputeReasonData': {'amount': 500}
})
api_key = 'meridian_abc123...:meridian_xyz789...'
key_id, secret = api_key.split(':')
signature = calculate_signature(method, url, json_data, secret)
files = {'attachment': open('/path/to/screenshot.jpg', 'rb')}
data = {
'reason': 'invalid_sum',
'description': 'Клиент отправил 500 RUB вместо 1000 RUB',
'disputeReasonData': json.dumps({'amount': 500})
}
response = requests.post(
url,
headers={'X-API-Key': api_key, 'X-Signature': signature},
data=data,
files=files
)
print(response.json())
Пример ответа
HTTP Status: 201 Created{
"id": "disp_abc123xyz789",
"paymentId": "cm3k8x7y80001z8j4k5m6n7o8",
"reason": "invalid_sum",
"description": "Клиент отправил 500 RUB вместо 1000 RUB",
"disputeReasonData": {
"amount": 500
},
"attachmentUrl": "https://meridian-disputes.s3.amazonaws.com/disp_abc123xyz789/screenshot.jpg",
"attachmentFilename": "screenshot.jpg",
"status": "open",
"resolution": null,
"resolutionNotes": null,
"createdAt": "2025-11-03T15:10:00.000Z",
"resolvedAt": null,
"autoResolveAt": "2025-11-03T16:10:00.000Z",
"amount": "1000",
"currency": "RUB",
"bankName": "sberbank",
"dealRequisites": "{\"bankName\":\"garant_bank\",\"cardNumber\":\"**** 1234\",\"fullName\":\"Ivan Ivanov\"}",
"internalId": "order-12345",
"merchantName": "MerchantName",
"clientEmail": "[email protected]"
}
Поля ответа апелляции
| Поле | Тип | Описание |
|---|---|---|
id | string | Уникальный идентификатор апелляции |
paymentId | string | ID платежа, по которому создана апелляция |
reason | string | Причина спора: invalid_sum, has_payment, invalid_requisites, unknown |
description | string | null | Описание проблемы |
disputeReasonData | object | null | Дополнительные данные. Для invalid_sum содержит {"amount": number} |
attachmentUrl | string | null | URL файла-доказательства (7 дней) |
attachmentFilename | string | null | Имя загруженного файла |
status | string | open (ожидает рассмотрения) или closed (разрешен) |
resolution | string | null | Решение: merchant_win, trader_win или null |
resolutionNotes | string | null | Комментарий администратора |
createdAt | string | Время создания. ISO 8601 |
resolvedAt | string | null | Время разрешения. ISO 8601 |
autoResolveAt | string | null | Время автоматического разрешения. ISO 8601 |
amount | string | Сумма платежа |
currency | string | Валюта (RUB) |
bankName | string | Запрошенный российский банк (не банк назначения) |
dealRequisites | string | JSON-строка с реквизитами. См. Структура dealRequisites |
internalId | string | null | Ваш идентификатор заказа |
merchantName | string | null | Название мерчанта |
clientEmail | string | null | Email клиента |
Автоматическое разрешение
Важно: Если апелляция не будет разрешена в течение 60 минут, система автоматически разрешит её в пользу мерчанта (
merchant_win).Проверка статуса апелляции
GET https://api.meridian.vip/api/v1/transgran/disputes/:id
Пример запроса
const method = 'GET';
const disputeId = 'disp_abc123xyz789';
const url = `https://api.meridian.vip/api/v1/transgran/disputes/${disputeId}`;
const signature = calculateSignature(method, url, '', secret);
const response = await fetch(url, {
method,
headers: {
'X-API-Key': apiKey,
'X-Signature': signature
}
});
const dispute = await response.json();
console.log('Status:', dispute.status);
console.log('Resolution:', dispute.resolution);
Пример ответа
{
"id": "disp_abc123xyz789",
"paymentId": "cm3k8x7y80001z8j4k5m6n7o8",
"reason": "invalid_sum",
"description": "Клиент отправил 500 RUB вместо 1000 RUB",
"disputeReasonData": {
"amount": 500
},
"attachmentUrl": "https://meridian-disputes.s3.amazonaws.com/disp_abc123xyz789/screenshot.jpg",
"attachmentFilename": "screenshot.jpg",
"status": "closed",
"resolution": "merchant_win",
"resolutionNotes": "Платеж подтвержден на сумму 500 RUB",
"createdAt": "2025-11-03T15:10:00.000Z",
"resolvedAt": "2025-11-03T15:30:00.000Z",
"autoResolveAt": "2025-11-03T16:10:00.000Z",
"amount": "1000",
"currency": "RUB",
"bankName": "sberbank",
"dealRequisites": "{\"bankName\":\"garant_bank\",\"cardNumber\":\"**** 1234\",\"fullName\":\"Ivan Ivanov\"}",
"internalId": "order-12345",
"merchantName": "MerchantName",
"clientEmail": "[email protected]"
}