Skip to content

How to Accept Card Payments and Make Payouts (H2H)

You need to accept payments from bank cards (Payin) and make payouts to cards (Payout), where card data is transmitted through your Gateway. You are implementing an H2H flow (Host-to-Host) — direct exchange of card data between your microservice and provider, without user involvement.


Incoming request contains card data in 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"
}
}

Full request structure is described in Basic Contracts.

Parameter gateway_amount is always passed in kopecks (10000 = 100.00 USD).

Use settings.api_key for authentication. Formulate payload according to your provider’s specification.

Working with settings parameters is described in Settings and Statuses.

Step 4. Handle Three Possible Response Scenarios

Section titled “Step 4. Handle Three Possible Response Scenarios”
Scenario Response Status Action
Payment successful "approved" Return result: true, status: "approved"
3DS required "pending" Return redirect_request with ACS server URL
Payment declined "declined" Return result: true, status: "declined" + details with reason

Example response with 3DS:

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

More details about redirect_request format — in Basic Contracts (section 3).

Record each HTTP request to provider in logs array:

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

Field kind accepts values: "pay", "payout", "status", "callback".


2. Implement Payout Processing (POST /payout)

Section titled “2. Implement Payout Processing (POST /payout)”

Unlike Payin, here params contains the recipient’s card (PAN only) or bank_account:

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

Convert gateway_amount from kopecks and send request according to provider’s specification.

If payout is instant:

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

If payout requires confirmation — return status: "pending" and wait for callback from provider.

More details about Payout request format — in Status and Payout.


For asynchronous payments (3DS, pending), implement an endpoint that queries the provider for current transaction status and returns it in normalized form:

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

Detailed specification — in Status and Payout.


4. Implement Callback Handling (POST /callback)

Section titled “4. Implement Callback Handling (POST /callback)”

When provider sends a webhook:

  1. Validate provider’s signature (mechanism depends on provider — JWT, RSA, HMAC).
  2. Normalize status (approved / declined).
  3. Send result to Business, signing with JWT key settings.sign_key.

Full callback and JWT instruction — in Callbacks & JWT. Error handling is described in Errors and Timeouts.


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,
},
],
});
});