EN RU

Merchant Integration API

Overview

This document describes the API for merchant integration with the Route payment platform. The API allows merchants to create payment requests (pay-in), receive status updates via webhooks, check payment statuses, and manage their account.

Base URL: https://route.black


Authorization

Obtaining Credentials

To begin integration, you must:

  1. Request access to the merchant dashboard from your account manager.
  2. A merchant account will be created for you. Each merchant can have one or more shops.
  3. Credentials are issued per shop, not per merchant. Each shop has its own API Key and Secret Key.
  4. Obtain your API Key (sk_live_...) and Secret Key (64-character hex string) from the shop settings page.
  5. Ensure the shop has a traffic group configured — this determines which trader pool handles your payments.
  6. Provide your account manager with the static public IP address(es) of the server(s) that will call the API and host your callback endpoint, so we can add them to the access allowlist (see IP Allowlisting).

Authentication Headers

All requests to the Merchant API must include the following headers:

Header Description
X-API-Key Your merchant API key (format: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
X-Signature HMAC-SHA256 signature of the request (see below)
Content-Type application/json for JSON requests

Signature Generation

The X-Signature header is a Base64-encoded HMAC-SHA256 signature computed from the concatenation of:

  1. HTTP method (GET, POST, etc.)
  2. Full request URL (including query parameters)
  3. Request body (only for application/json requests; empty string for GET requests)

Signature formula:

X-Signature = Base64(HMAC-SHA256(secret_key, METHOD + URL + BODY))

Code Examples

PHP:

<?php
$method = 'POST';
$url = 'https://route.black/api/v1/payments';
$body = json_encode(['currency' => 'RUB', 'amount' => 5000]);
$secretKey = 'your_secret_key';

$stringToSign = $method . $url . $body;
$signature = base64_encode(hash_hmac('sha256', $stringToSign, $secretKey, true));

// Set header: X-Signature: $signature

JavaScript (Node.js):

const crypto = require('crypto');

const method = 'POST';
const url = 'https://route.black/api/v1/payments';
const body = JSON.stringify({ currency: 'RUB', amount: 5000 });
const secretKey = 'your_secret_key';

const stringToSign = method + url + body;
const signature = crypto
  .createHmac('sha256', secretKey)
  .update(stringToSign)
  .digest('base64');

// Set header: X-Signature: <signature>

Python:

import hmac
import hashlib
import base64
import json

method = 'POST'
url = 'https://route.black/api/v1/payments'
# Use separators=(',', ':') for compact JSON — must match the body sent in the request exactly
body = json.dumps({'currency': 'RUB', 'amount': 5000}, separators=(',', ':'))
secret_key = 'your_secret_key'

string_to_sign = method + url + body
signature = base64.b64encode(
    hmac.new(
        secret_key.encode(),
        string_to_sign.encode(),
        hashlib.sha256
    ).digest()
).decode()

# Set header: X-Signature: <signature>

Important: The signature is computed over the exact bytes of the request body. Ensure that the JSON you sign and the JSON you send are byte-for-byte identical. Use compact serialization (no extra spaces) to avoid inconsistencies across languages.


Error Response Format

All errors from the API follow a single envelope:

{
  "success": false,
  "error": "<token>"
}

The error field is a stable, snake_case token — never raw English text. Merchant integrators should switch on this token to render localised messages or branch their retry/escalation logic. The HTTP status code carries the broad class (400 / 401 / 404 / 409 / 410 / 422 / 500), and the token carries the precise reason.

Conventions

Canonical Token List

The following tokens are emitted by the payment endpoints documented below. Each row pins the HTTP status code, the token, and the trigger.

Status Token When
400 currency_mismatch Request currency does not match the shop's allowed currency
400 amount_below_minimum amount < shop min_amount_in
400 amount_above_maximum amount > shop max_amount_in
400 duplicate_external_id external_id already used by this merchant (when shop has allow_duplicate_invoices=false)
400 callback_url_required No callback_url in the request and none configured on the shop
401 unauthorized Missing / invalid X-API-Key or X-Signature
404 payment_not_found GET /api/v1/payments/{id} for an ID that does not exist or that the caller does not own
409 payment_not_pending Operation requires waiting_sms/created state, but the payment is already confirmed/failed/expired
410 payment_expired Payment's expires_at has passed (RFC 7231 §6.5.9 Gone)
500 internal_error Sanitised server-side failure

What is NOT in this table. Routing-side conditions ("no trader available", "shop not configured", "balance low", "cascade group missing", etc.) are deliberately absent. They never reach the merchant as 4xx — they all collapse into the same empty 200 OK response. See the no-capacity bullet above.


Host-to-Host Integration

Host-to-Host integration enables direct server-to-server communication between the merchant's backend and the payment platform API, allowing full automation of payment processing.

IP Allowlisting

For security, access to the Merchant API is restricted to approved source IP addresses. Before going live, provide your account manager with the static public IP address(es) or CIDR ranges of every server that will:

Requests originating from IP addresses that are not on the allowlist may be rejected before they reach the API. If your infrastructure scales or your egress IP addresses change, notify us in advance so we can update the allowlist and avoid any interruption to your integration.

Receiving callbacks: platform callbacks are delivered from a fixed set of outbound IP addresses. If your callback endpoint sits behind a firewall, request our current outbound IP ranges from your account manager and allow them on your side.

Pay-In Request Creation

The pay-in flow allows merchants to accept payments from their customers.

Flow Overview

1. Merchant server  -> POST /api/v1/payments       -> Creates payment, receives requisites
2. Merchant         -> Displays requisites to user  -> User transfers funds
3. Platform         -> Detects incoming transfer    -> Confirms payment
4. Platform         -> POST to merchant callback    -> Notifies merchant of status change
5. Merchant         -> GET /api/v1/payments/{id}    -> (Optional) Verify final status

Step 1: Create Payment

The merchant sends a payment creation request. The system routes the payment to an available trader, selects appropriate requisites (card number or SBP phone), and returns them in the response.

Important — three response classes:

  1. 201 Created + populated data — requisites issued, show them to the customer.
  2. 200 OK + data: null — the platform cannot currently issue requisites. The cause is never exposed: no trader, no requisite, balance issues, shop block, cascade gap, provider error — all surface as the same empty response. Retry with backoff or offer the customer an alternative method.
  3. 4xx + { "error": "<token>" } — caller-correctable rejection of the request itself (bad currency, amount out of shop range, duplicate external_id, missing callback_url, etc.). See Error Response Format.

Anything else (5xx) is a transient platform failure — the body is sanitised to internal_error and the call should be retried.

Request:

POST /api/v1/payments
Content-Type: application/json
X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Signature: <computed_signature>
{
  "currency": "RUB",
  "amount": 5000.00,
  "external_id": "order-12345",
  "callback_url": "https://merchant.example.com/webhooks/payments",
  "payment_method": "sbp"
}

currency, amount and payment_method are required. The external_id and callback_url fields are optional (see the request body table under Create Payment).

Response (201 Created):

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "waiting_sms",
    "currency": "RUB",
    "amount": 5000.00,
    "amount_usdt": 52.63,
    "external_id": "ext-unique-id",
    "requisite": {
      "type": "card",
      "card_number": "2200 1234 5678 9012",
      "card_holder": "IVAN IVANOV",
      "bank_name": "Sberbank",
      "bank_code": "sberbank",
      "timer_seconds": 900
    },
    "expires_at": "2026-02-15T15:15:00Z",
    "created_at": "2026-02-15T15:00:00Z"
  }
}

The merchant must display the returned requisites to the customer and instruct them to transfer the exact amount. The payment expires after the timer_seconds period (default 15 minutes).

Possible requisite types:

Step 2: Payment Confirmation

Once the customer completes the transfer, the platform automatically detects the incoming payment. No manual confirmation endpoint is required from the merchant side — the system handles detection automatically via its internal network.

Step 3: Webhook Notification

Upon payment confirmation, the platform sends a webhook to the merchant's configured callback_url. See the Callbacks section for details.

Pay-Out Request Creation

Note: Pay-out functionality is currently under development. Contact your account manager for the timeline and early access.

The pay-out flow will allow merchants to send funds to customer bank accounts or cards.


Callbacks (Webhooks)

Overview

The platform sends HTTP POST callbacks to the merchant's callback_url whenever a payment status changes. The callback URL is configured in the merchant profile (not per-request).

Authentication

Each webhook request includes an X-Signature header computed using the merchant's secret key:

X-Signature = Base64(HMAC-SHA256(secret_key, "POST" + webhook_url + body))

The merchant must validate this signature to ensure the webhook is authentic and has not been tampered with.

Webhook Payload

{
  "payment_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "external_id": "ext-unique-id",
  "status": "confirmed",
  "currency": "RUB",
  "amount": 5000.00,
  "amount_usdt": 52.63,
  "tx_id": "",
  "confirmed_at": "2026-02-15T15:05:30Z",
  "timestamp": "2026-02-15T15:05:31Z"
}

Webhook Payload Fields

Field Type Description
payment_id string (UUID) Unique payment identifier in the platform
external_id string External reference ID
status string New payment status (see PaymentStatus)
currency string Payment currency (e.g., RUB)
amount number Payment amount in original currency
amount_usdt number Payment amount converted to USDT
tx_id string Blockchain transaction ID (when completed)
confirmed_at string (ISO 8601) Confirmation timestamp
timestamp string (ISO 8601) Webhook generation timestamp

Expected Response

The merchant must respond with HTTP status code 200 OK. Any other status code is considered a failure, and the webhook will be retried.

Retry Policy

If the merchant's server fails to respond with 200, the platform retries with exponential backoff:

Attempt Delay
1 5 minutes
2 15 minutes
3 1 hour
4 6 hours
5-10 24 hours

Maximum 10 retry attempts. After exhaustion, the webhook is marked as failed and can be manually retried through the dashboard or API.

Signature Verification Example

PHP:

<?php
$secretKey = 'your_secret_key';
$webhookUrl = 'https://yoursite.com/webhook';
$body = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_SIGNATURE'];

$stringToSign = 'POST' . $webhookUrl . $body;
$expectedSignature = base64_encode(
    hash_hmac('sha256', $stringToSign, $secretKey, true)
);

if (!hash_equals($expectedSignature, $receivedSignature)) {
    http_response_code(403);
    exit('Invalid signature');
}

// Process the webhook...
http_response_code(200);

Python:

import hmac
import hashlib
import base64

secret_key = 'your_secret_key'
webhook_url = 'https://yoursite.com/webhook'
body = request.get_data(as_text=True)
received_signature = request.headers.get('X-Signature')

string_to_sign = 'POST' + webhook_url + body
expected_signature = base64.b64encode(
    hmac.new(
        secret_key.encode(),
        string_to_sign.encode(),
        hashlib.sha256
    ).digest()
).decode()

if not hmac.compare_digest(expected_signature, received_signature):
    return 'Invalid signature', 403

# Process the webhook...
return 'OK', 200

Disputes (Appeals)

A dispute (appeal) is opened when a customer claims they completed a transfer but the payment was not credited to them — for example, they paid after the invoice expired, or to a requisite the system did not automatically match.

Important: merchants have no dedicated dispute API. Intake and resolution happen entirely on the platform side — the merchant only observes the outcome.

How it works

  1. Intake. The customer raises the appeal through platform support, providing the amount, the requisite (last 4 digits of the card or the phone number), the time of payment, and optionally supporting evidence (receipt/screenshot).
  2. Matching. The platform searches for the corresponding payment by requisite, exact amount, and time (±2 hours). If exactly one is found, the payment moves to dispute status and the appeal is linked to it.
  3. Moderation. A platform operator reviews the evidence and makes a decision.

Outcome and merchant impact

Decision What happens to the payment Merchant notification
Approved The payment is settled and moves to confirmed Standard webhook with status confirmedincluding for a payment previously reported as expired
Rejected The payment returns to expired No additional webhook

Integration note — late confirmation. Because of disputes, a payment you already received as expired may later arrive as confirmed. Do not treat expired as unconditionally final: process the confirmed webhook idempotently even when it arrives after expired, reconciling by payment_id / external_id.

The transition into dispute itself is not delivered as a separate webhook — you can observe this status by polling GET /api/v1/payments/{id}.


API Reference

Create Payment

Creates a new pay-in payment request and returns transfer requisites.

Endpoint: POST /api/v1/payments

Authentication: API Key + Signature

Request Headers:

Header Required Description
X-API-Key Yes Merchant API key
X-Signature Yes Request signature
Content-Type Yes application/json

Request Body:

Parameter Type Required Description
currency string Yes Currency code, 3 characters (e.g., RUB, KZT, USD, EUR)
amount number Yes Payment amount (minimum: 100)
payment_method string Yes Payment method — selects the routing (traffic group). One of card (C2C), sbp (SBP), transgran (cross-border cards).
external_id string No Your own order/invoice ID for correlation. Auto-generated UUID if omitted. Must be unique per merchant.
callback_url string No Webhook URL for this payment's status notifications. Overrides the default callback_url from merchant profile.
shop_id string (UUID) No Target shop. Optional: if omitted, the platform picks your merchant's shop by currency (+ payment_method). Pass shop_id (see GET /api/v1/shops) only when more than one shop matches — otherwise shop_ambiguous is returned. Must belong to your merchant, otherwise shop_not_owned.
customer_id string Yes Stable identifier of your customer (the payer), the same across all their payments. Used by antifraud for per-user history and blocking. If omitted — customer_id is required. If the user is blocked at your merchant, creating a payment returns user_blocked.
customer_phone string No Payer phone (optional).
customer_email string No Payer email (optional).
device_id string No Payer device identifier (optional).

Request Example:

{
  "currency": "RUB",
  "amount": 5000.00,
  "external_id": "order-12345",
  "callback_url": "https://merchant.example.com/webhooks/payments",
  "payment_method": "sbp"
}

Response (201 Created):

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "waiting_sms",
    "currency": "RUB",
    "amount": 5000.00,
    "amount_usdt": 52.63,
    "external_id": "generated-uuid",
    "requisite": {
      "type": "card",
      "card_number": "2200 1234 5678 9012",
      "card_holder": "IVAN IVANOV",
      "bank_name": "Sberbank",
      "bank_code": "sberbank",
      "timer_seconds": 900
    },
    "expires_at": "2026-02-15T15:15:00Z",
    "created_at": "2026-02-15T15:00:00Z"
  }
}

Error Responses:

The response body is {"success": false, "error": "<token>"} — see Error Response Format for the full token list. The most relevant tokens for this endpoint are:

Status Token Trigger
400 currency_mismatch currency differs from shop's allowed currency
400 amount_below_minimum amount < shop min_amount_in
400 amount_above_maximum amount > shop max_amount_in
400 duplicate_external_id external_id already used by this merchant
400 callback_url_required No callback_url in request and none configured on the shop
400 shop_ambiguous shop_id omitted but more than one shop matches currency+payment_method — pass shop_id
400 invalid_shop_id shop_id is malformed or the shop was not found
400 shop_not_owned The given shop_id does not belong to your merchant
400 shop_id_conflict The key is shop-scoped but a different shop_id was sent in the body
401 unauthorized Missing/invalid X-API-Key or X-Signature
500 internal_error Transient server failure — retry

No-capacity is not an error and never reveals the cause. When the platform cannot issue requisites — for any reason at all (no trader, no requisite, balance, shop block, cascade gap, provider error, …) — the response is HTTP 200 with data: null. The 4xx tokens above are limited to syntactic / caller-correctable issues. See Error Response Format.


List Shops

Returns your merchant's shops. Use it to discover the shop_id required by the shop_id field in Create Payment.

Endpoint: GET /api/v1/shops

Authentication: API key (X-API-Key) — the X-Signature header is not required for this GET request. Works with both a unified merchant key and a shop-bound key (both return all of the merchant's shops).

Response (200 OK):

{
  "success": true,
  "data": [
    {
      "shop_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "My Shop",
      "currency": "RUB",
      "methods": ["sbp"],
      "payment_types": ["card", "sbp"],
      "active": true
    }
  ]
}
Field Description
shop_id Shop UUID — pass it in the POST /api/v1/payments body
name Shop name
currency Shop currency (may be empty if unset)
methods Allowed pay-in methods (e.g. sbp, card)
payment_types Allowed payment types
active true if the shop is active and pay-in is not blocked

Error responses:

Status Token Condition
401 unauthorized Missing/invalid X-API-Key
500 internal_error Transient server failure — retry

Get Payment by ID

Retrieves the current status and details of a payment.

Endpoint: GET /api/v1/payments/{id}

Authentication: API Key (X-API-Key) or JWT Bearer Token — no X-Signature required for GET requests

URL Parameters:

Parameter Type Description
id string (UUID) Payment identifier

Response (200 OK):

{
  "success": true,
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "confirmed",
    "currency": "RUB",
    "amount": 5000.00,
    "amount_usdt": 52.63,
    "external_id": "generated-uuid",
    "created_at": "2026-02-15T15:00:00Z",
    "updated_at": "2026-02-15T15:05:30Z"
  }
}

Error Responses:

Status Token Trigger
400 invalid_id Path parameter is not a valid UUID
401 unauthorized Missing/invalid X-API-Key
404 payment_not_found No payment with that ID/external_id exists for this merchant
500 internal_error Transient server failure — retry

Get Merchant Payments

Retrieves a paginated list of payments for the authenticated merchant.

Endpoint: GET /api/v1/merchants/{merchant_id}/payments

Authentication: JWT Bearer Token

URL Parameters:

Parameter Type Description
merchant_id string (UUID) Merchant identifier

Query Parameters:

Parameter Type Default Description
page integer 1 Page number
page_size integer 20 Items per page (max: 100)

Response (200 OK):

{
  "success": true,
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "status": "confirmed",
      "currency": "RUB",
      "amount": 5000.00,
      "amount_usdt": 52.63,
      "external_id": "generated-uuid",
      "created_at": "2026-02-15T15:00:00Z"
    }
  ],
  "pagination": {
    "total": 150,
    "page": 1,
    "page_size": 20,
    "total_pages": 8
  }
}

Webhook Management

Endpoints for monitoring and managing webhook delivery.

Authentication: JWT Bearer Token

Get Webhook Statistics

Endpoint: GET /api/v1/merchant/webhooks/stats

Response:

{
  "success": true,
  "data": {
    "pending": 2,
    "processing": 0,
    "completed": 145,
    "failed": 3
  }
}

Get Failed Webhooks

Endpoint: GET /api/v1/merchant/webhooks/failed

Retry Failed Webhook

Endpoint: POST /api/v1/merchant/webhooks/{id}/retry

Get Webhook Details

Endpoint: GET /api/v1/merchant/webhooks/{id}


API Object Descriptions

PaymentStatus

Status Description
created Payment has been created, trader assignment in progress
waiting_sms Requisites assigned, waiting for customer to transfer funds
confirmed Payment confirmed — funds received and verified
completed Payment fully completed — settlement processed
failed Payment failed at any processing stage
expired Payment not completed within the timeout period (default: 15 min)
dispute A dispute (appeal) has been opened for the payment — under review. See Disputes (Appeals)

PaymentResponse

Field Type Description
id string (UUID) Unique payment identifier
status PaymentStatus Current payment status
currency string Payment currency code (e.g., RUB)
amount number Payment amount in original currency
amount_usdt number Payment amount converted to USDT
external_id string External reference identifier
requisite RequisiteDTO Transfer requisites (when status is waiting_sms)
expires_at string (ISO 8601) Payment expiration timestamp
created_at string (ISO 8601) Payment creation timestamp

RequisiteDTO

Field Type Description
type string Requisite type: card, sbp or transgran
phone_number string SBP phone number (for type sbp)
card_number string Bank card number (for type card / transgran)
card_holder string Cardholder name (for type card)
bank_name string Bank name
bank_code string Bank identifier code
timer_seconds integer Time in seconds before payment expires

WebhookPayload

Field Type Description
payment_id string (UUID) Payment identifier
external_id string External reference ID
status PaymentStatus Updated payment status
currency string Payment currency
amount number Amount in original currency
amount_usdt number Amount in USDT
tx_id string Blockchain transaction hash (on completion)
confirmed_at string (ISO 8601) Confirmation timestamp
timestamp string (ISO 8601) Webhook timestamp

Error Handling

Response Format

All error responses follow the same format:

{
  "success": false,
  "error": "Error description message"
}

HTTP Status Codes

Code Description
200 Success
201 Created successfully
400 Bad Request — invalid parameters or business logic violation
401 Unauthorized — invalid or missing API key / signature
404 Not Found — requested resource does not exist
409 Conflict — duplicate request (idempotency)
500 Internal Server Error

Common Error Messages

Error Cause
Invalid request body Malformed JSON or missing required fields
currency must be 3 characters Currency code not in ISO 4217 format
Merchant not authenticated Missing or invalid X-API-Key header
Invalid signature X-Signature does not match computed value
Merchant has no callback URL configured Merchant profile missing callback_url
Payment not found or already processed Payment ID not found or in terminal state
Payment is not waiting for SMS confirmation Payment in unexpected status

Supported Currencies

Code Currency
RUB Russian Ruble
USD US Dollar
EUR Euro
THB Thai Baht

Note: Available currencies depend on the merchant's traffic group configuration. Contact your account manager for activation.


Rate Limits

API requests are rate-limited per merchant. Default limits:

Endpoint Limit
POST /payments 60 requests/minute
GET /payments/* 120 requests/minute

Exceeding rate limits returns HTTP 429 (Too Many Requests).


Testing and Sandbox

For integration testing, use the sandbox environment provided by your account manager. Sandbox API keys use the prefix sk_test_.

All flows work identically to production, but no real money movement occurs.