Перейти к содержимому

Как принимать платежи с карты и делать выплаты (H2H)

Вам нужно принимать платежи с банковских карт (Payin) и делать выплаты на карты (Payout), при этом данные карты передаются через ваш Gateway. Вы реализуете H2H flow (Host-to-Host) — прямой обмен данными карты между вашим микросервисом и провайдером, без участия пользователя.


Входящий запрос содержит данные карты в params:

{
"params": {
"pan": "4111111111111111",
"cvv": "123",
"expires": "12/25",
"customer": { "email": "user@example.com", "ip": "192.168.1.1" }
},
"payment": {
"token": "tok_xxxx",
"gateway_amount": 10000,
"gateway_currency": "USD"
},
"settings": {
"api_key": "provider_api_token"
}
}

Полная структура запроса описана в Базовые Контракты.

Параметр gateway_amount всегда передаётся в копейках (10000 = 100.00 USD).

Используйте settings.api_key для аутентификации. Сформируйте payload под спецификацию вашего провайдера.

Работа с параметрами settings описаны в Настройки и Статусы.

Шаг 4. Обработайте три возможных сценария ответа

Заголовок раздела «Шаг 4. Обработайте три возможных сценария ответа»
Сценарий Статус в ответе Действие
Платёж успешен "approved" Вернуть result: true, status: "approved"
Требуется 3DS "pending" Вернуть redirect_request с URL ACS-сервера
Платёж отклонён "declined" Вернуть result: true, status: "declined" + details с причиной

Пример ответа с 3DS:

{
"result": true,
"status": "pending",
"gateway_token": "txn_12345",
"redirect_request": {
"url": "https://acs-bank.com/challenge",
"type": "post",
"params": { "creq": "eyJhbGciOiJIUzI1NiIs..." }
},
"logs": [...]
}

Подробнее про формат redirect_request — в Базовые Контракты (секция 3).

Каждый HTTP-запрос к провайдеру зафиксируйте в массиве logs:

{
"request": { "url": "...", "body": "..." },
"status": 200,
"response": "{...}",
"kind": "pay",
"duration": 0.35
}

Поле kind принимает значения: "pay", "payout", "status", "callback".


В отличие от Payin, здесь в params передаётся карта получателя (только PAN) или bank_account:

{
"params": {
"card": { "pan": "4111111111111111" },
"customer": { "email": "receiver@example.com" }
},
"payment": {
"token": "payout_tok_xxxx",
"gateway_amount": 5000,
"gateway_currency": "EUR"
}
}

Сконвертируйте gateway_amount из копеек и отправьте запрос согласно спецификации провайдера.

Если выплата мгновенная:

{
"result": true,
"status": "approved",
"gateway_token": "payout_12345",
"logs": [...]
}

Если выплата требует подтверждения — верните status: "pending" и дождитесь коллбека от провайдера.

Подробнее про формат Payout-запроса — в Status и Payout.


Для асинхронных платежей (3DS, pending) реализуйте эндпоинт, который запрашивает у провайдера текущий статус транзакции и возвращает его в нормализованном виде:

{
"result": true,
"status": "approved",
"amount": 10000,
"currency": "USD",
"logs": [...]
}

Детальная спецификация — в Status и Payout.


4. Реализуйте обработку коллбеков (POST /callback)

Заголовок раздела «4. Реализуйте обработку коллбеков (POST /callback)»

Когда провайдер присылает вебхук:

  1. Валидируйте подпись провайдера (механизм зависит от провайдера — JWT, RSA, HMAC).
  2. Нормализуйте статус (approved / declined).
  3. Отправьте результат в Business, подписав JWT ключом settings.sign_key.

Полная инструкция по коллбекам и JWT — в Callbacks & JWT. Обработка ошибок описана в Ошибки и Таймауты.


app.post("/pay", async (req, res) => {
const body = req.body;
const amount = body.payment.gateway_amount / 100;
const apiKey = body.settings.api_key;
const providerPayload = {
amount,
currency: body.payment.gateway_currency,
card: {
number: body.params.pan,
cvv: body.params.cvv,
expiry: body.params.expires,
},
customer: body.params.customer,
reference: body.payment.token,
};
const start = Date.now();
const providerRes = await fetch("https://api.provider.com/v1/charge", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(providerPayload),
});
const providerData = await providerRes.json();
const duration = (Date.now() - start) / 1000;
let response = {
result: true,
gateway_token: providerData.id,
amount: body.payment.gateway_amount,
currency: body.payment.gateway_currency,
logs: [
{
request: { body: providerPayload },
status: providerRes.status,
response: providerData,
kind: "pay",
duration,
},
],
};
if (providerData.status === "success") {
response.status = "approved";
response.details = "Payment successful";
} else if (providerData.status === "3ds_required") {
response.status = "pending";
response.redirect_request = {
url: providerData.acs_url,
type: "post",
params: { creq: providerData.creq },
};
} else {
response.status = "declined";
response.details = providerData.message || "Payment declined";
}
res.status(200).json(response);
});
app.post("/payout", async (req, res) => {
const body = req.body;
const amount = body.payment.gateway_amount / 100;
const apiKey = body.settings.api_key;
const providerPayload = {
amount,
currency: body.payment.gateway_currency,
card: body.params.card?.pan,
bank_account: body.params.bank_account,
customer: body.params.customer,
reference: body.payment.token,
};
const providerRes = await fetch("https://api.provider.com/v1/payouts", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(providerPayload),
});
const data = await providerRes.json();
res.status(200).json({
result: true,
status: data.status === "success" ? "approved" : "pending",
gateway_token: data.id,
amount: body.payment.gateway_amount,
currency: body.payment.gateway_currency,
logs: [
{
request: { body: providerPayload },
status: providerRes.status,
response: data,
kind: "payout",
duration: 0.5,
},
],
});
});