Payin H2H

HTTP API

Host-to-host контракт Merchant Payment API v1.

HTTP API

Base URL: /api/v1/merchant/pay

Краткая сводка по endpoint'ам

МетодEndpointНазначение
POST/availableПолучение доступных сумм встречных предложений
POST/newИнициация нового платежа
POST/new-asyncИнициация нового платежа (с асинхронным получение реквизитов)
POST/confirmПодтверждение пользователем, что он совершил оплату
POST/cancelОтмена платежа
POST/statusПолучение текущего статуса платежа
POST/receipt/attachПрикрепление одного или нескольких чеков к платежу
Webhook POSTcredentialsWebhookUrlWebhook для предоставления реквизитов
Webhook POSTstatusWebhookUrlWebhook для уведомления об изменении статуса

Заголовки безопасности и подписи

Все взаимодействия с API требуют использования криптографической подписи.

Запросы клиента к API

Каждый запрос должен содержать следующие заголовки:

  • X-KeyId — идентификатор ключа, полученный при создании мерчанта
  • X-Signature — подпись тела запроса, полученная с помощью приватного ключа

Внимание: значения X-KeyId и X-Signature, используемые клиентом и сервером, всегда различаются, так как используются разные ключи.

Ответы сервера

Ответы сервера также подписываются:

  • Заголовки X-KeyId и X-Signature присутствуют в ответе.
  • Подпись (X-Signature) создается на основе тела ответа, закодирована в base64 и может быть проверена публичным ключом сервера.

Webhook-запросы от сервера

Webhook-запросы также подписываются нашим приватным ключом. Вы можете проверять их с помощью предоставленного нами публичного ключа.

Алгоритм и генерация ключей

  • Алгоритм: ed25519
  • Публичный ключ (в base64) передаётся на сервер вручную
  • Не используйте openssl для генерации ed25519, т.к. он добавляет лишние метаданные
  • Рекомендуется использовать библиотеку соответствующего языка

Пример генерации пары ключей ed25519

package main

import (
    "crypto/ed25519"
    "encoding/base64"
    "fmt"
)

func main() {
    pub, priv, err := ed25519.GenerateKey(nil)
    if err != nil {
        panic(err)
    }

    pubBase64 := base64.StdEncoding.EncodeToString([]byte(pub))
    privBase64 := base64.StdEncoding.EncodeToString([]byte(priv))

    fmt.Printf("Public key base64:  %s\nPrivate key base64: %s", pubBase64, privBase64)
}

Пару ключей в base64 можно получить, запустив этот код на Go Playground.

Примеры формирования подписи

Go

import (
    "crypto/ed25519"
    "encoding/base64"
    "os"
)

func signMessage(msg []byte, privateKeyPath string) (string, error) {
    privKeyBytes, err := os.ReadFile(privateKeyPath)
    if err != nil {
        return "", err
    }
    priv := ed25519.PrivateKey(privKeyBytes)
    signature := ed25519.Sign(priv, msg)
    return base64.StdEncoding.EncodeToString(signature), nil
}

На Python

import base64
from nacl.signing import SigningKey

with open("private.key", "rb") as f:
    sk = SigningKey(f.read())

message = b'{"example":"data"}'
signed = sk.sign(message)
signature = base64.b64encode(signed.signature).decode()

# signature прикрепляется как заголовок X-Signature

На JavaScript (Node.js)

const nacl = require('tweetnacl');
const bs58 = require('bs58');
const bs64 = require('base64-js');
const fs = require('fs');

const privateKey = fs.readFileSync('private.key');
const message = Buffer.from(JSON.stringify({ example: 'data' }));

const keyUint8 = new Uint8Array(privateKey);
const signature = nacl.sign.detached(message, keyUint8);
const signatureBase64 = bs64.fromByteArray(signature);

// signatureBase64 прикрепляется как заголовок X-Signature

На Javascript

<script src="https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>

function generateSignature(data, privateKeyBase64) {
    if (typeof nacl === 'undefined') {
        throw new Error('Библиотека tweetnacl не загружена. Добавьте её в Collection Scripts.');
    }

    console.log("Data for signing:", data);
    console.log("Data type:", typeof data);

    // ВАЖНО: В Go примере подписывается JSON как есть, БЕЗ сортировки!
    // Если data - строка, используем её как есть
    // Если data - объект, преобразуем в JSON строку
    let jsonString;

    if (typeof data === 'string') {
        // Проверяем, валидный ли это JSON
        try {
            JSON.parse(data); // Только проверка
            jsonString = data; // Используем как есть
        } catch (e) {
            // Если не JSON, преобразуем объект в строку
            jsonString = JSON.stringify(data);
        }
    } else {
        // Если это объект, преобразуем в JSON строку
        jsonString = JSON.stringify(data);
    }

    console.log("Final JSON string:", jsonString);

    // Декодируем приватный ключ
    const keyUint8Array = nacl.util.decodeBase64(privateKeyBase64);
    console.log("Private key length:", keyUint8Array.length);

    // Подписываем строку как есть (без сортировки!)
    const messageUint8Array = nacl.util.decodeUTF8(jsonString);
    const signature = nacl.sign.detached(messageUint8Array, keyUint8Array);

    const signatureBase64 = nacl.util.encodeBase64(signature);
    console.log("Generated signature:", signatureBase64);

    return signatureBase64;
}

Endpoints

POST /available

Описание: Возвращает список доступных сумм встречных предложений для заданных валюты и метода оплаты.

Content-Type: application/json

Пример запроса

{
  "sum": 100.5,
  "currency": "RUB",
  "paymentMethod": "card", // "card" | "sbp". Обязательно.
  "customerAccount": "string", // Опционально; сейчас не влияет на подбор сумм.
  "sumDriftUp": 5, // Опционально. Допустимое отклонение суммы вверх.
  "sumDriftDown": 5 // Опционально. Допустимое отклонение суммы вниз.
}

Успешный ответ

{
  "success": true,
  "availableAmounts": [100.5, 105.0, 110.0]
}

Ошибка

{
  "success": false,
  "errorDescription": "invalid currency"
}

При ошибке парсинга JSON или sum <= 0 ответ приходит в упрощённом виде {"error": "..."} (без полей success / errorDescription).


POST /new

Описание: Инициирует новый платёж.

Content-Type: application/json

Пример запроса

{
  "sum": 100.5,
  "currency": "RUB", // Only "RUB".
  "paymentMethod": "card", // "card" | "sbp".
  "statusWebhookUrl": "https://merchant.com/status", // Опционально.
  "customerAccount": "string",
  "payerBank": "100000000111" // Опционально (см. примечание ниже). Код банка плательщика (отправителя)
}
  • Опциональные поля: statusWebhookUrl, payerBank. Остальные — обязательные.
  • Поле payerBank может быть обязательным по условиям работ.

Успешный ответ

{
  "success": true,
  "orderId": "abc123",
  "sum": 100,
  "payeeAccount": "1234567890",
  "payeeAccountBank": "Bank",
  "payeeName": "HolderName",
  "currency": "RUB",
  "dealSum": 90,
  "dealCurrency": "RUB",
  "dealRate": 0.9,
  "deeplink": "https://deeplink.example.com/frame/abc123"
}

Ошибка

{
  "success": false,
  "errorDescription": "invalid currency"
}

POST /new-async

Описание: Инициирует новый платёж.

Content-Type: application/json

Пример запроса

{
  "sum": 100.5,
  "currency": "RUB", // Only "RUB".
  "paymentMethod": "card", // "card" | "sbp".
  "credentialsWebhookUrl": "https://merchant.com/credentials",
  "statusWebhookUrl": "https://merchant.com/status", // Опционально.
  "customerAccount": "string",
  "payerBank": "100000000111" // Опционально (см. примечание ниже). Код банка плательщика (отправителя)
}
  • Опциональные поля: statusWebhookUrl, payerBank. Остальные — обязательные.
  • Поле payerBank может быть обязательным по условиям работ.

Успешный ответ

{
  "success": true,
  "orderId": "abc123"
}

Ошибка

{
  "success": false,
  "errorDescription": "invalid currency"
}

POST /confirm

Описание: Пользователь подтверждает, что совершил платеж. Инициирует проверку платежа со стороны нашей системы.

Content-Type: application/json

Пример запроса

{
  "orderId": "abc123"
}

Успешный ответ

{
  "success": true,
  "orderId": "abc123"
}

Ошибка

{
  "success": false,
  "errorDescription": "invalid orderId"
}

POST /cancel

Описание: Отменяет ранее созданный платёж.

Ограничение: Отмена возможна только пока платёж ожидает подтверждения оплаты пользователем либо находится на стадии арбитража (включая receiptRequired). В остальных случаях — в том числе для финальных статусов doneSuccess / doneFail — сервер возвращает HTTP 403 с errorDescription: "payment could not be canceled".

Content-Type: application/json

Пример запроса

{
  "orderId": "abc123"
}

Успешный ответ

{
  "success": true,
  "orderId": "abc123"
}

Ошибка

{
  "success": false,
  "orderId": "abc123",
  "errorDescription": "invalid orderId"
}

POST /status

Описание: Получение текущего статуса платежа по orderId. Возможные статусы:

Content-Type: application/json

Пример запроса

{
  "orderId": "abc123"
}

Успешный ответ

{
  "success": true,
  "orderId": "abc123",
  "sum": 100,
  "payeeAccount": "1234567890",
  "payeeAccountBank": "Bank",
  "payeeName": "HolderName",
  "dealSum": 90,
  "dealCurrency": "RUB",
  "dealRate": 0.9,
  "deeplink": "https://deeplink.example.com/frame/abc123",
  "status": "pending"
}

Примечание: поле payeeName в ответе /status возвращается транслитерированным в латиницу (например, ИвановIvanov).

Ошибка

{
  "success": false,
  "errorDescription": "order not found"
}

Возможные статусы status:

  • free - Заявка была успешна создана, но подходящие платёжные реквизиты пока не найдены.
  • pending - Платеж находится в процессе обработки (реквизиты ищутся или ожидается оплата пользователем).
  • receiptRequired - Для завершения платежа требуется прикрепить чек.
  • doneSuccess - Платёж успешно завершён.
  • doneFail - Платёж завершён с ошибкой.
  • arbitrageCancelledByMerchant - Платёж отменён мерчантом на стадии арбитража (финальный статус).

Webhook: credentialsWebhookUrl (при использовании асинхронного метода)

Описание: Вызывается сервером для выдачи реквизитов.

Успешный запрос

{
  "success": true,
  "orderId": "abc123",
  "sum": 100.5,
  "payeeAccount": "1234567890",
  "payeeAccountBank": "Bank",
  "payeeName": "HolderName",
  "currency": "RUB",
  "dealSum": 90,
  "dealCurrency": "RUB",
  "dealRate": 0.9,
  "deeplink": "https://deeplink.example.com/frame/abc123"
}

Ошибка

{
  "success": false,
  "orderId": "abc123"
}

Webhook: statusWebhookUrl (опционально)

Описание: Уведомление об новом статусе платежа.

Пример:

{
  "success": true,
  "orderId": "abc123",
  "status": "doneSuccess",
  "dealSum": 90,
  "dealCurrency": "RUB",
  "dealRate": 0.9,
  "deeplink": "https://deeplink.example.com/frame/abc123"
}

Обратите внимание: через данный вебхук могут быть переданы только следующие статусы: receiptRequired, doneSuccess, doneFail


POST /receipt/attach

Описание: Прикрепление чека.

Content-Type: multipart/form-data

ПолеТипОбяз.Описание
orderIdstringдаИдентификатор заказа
fileфайлыдаЧеки. Формат определяется по Content-Type: application/pdf, image/jpeg, image/jpg, image/png

Пример с curl:

curl -X POST https://host/api/v1/merchant/pay/receipt/attach   -F "orderId=abc123"   -F "file=@receipt.pdf"

Успешный ответ

{
  "success": true,
  "orderId": "abc123"
}

Ошибка

{
  "success": false,
  "errorDescription": "invalid file format"
}

Currencies

const (
    CurrencyRUB = "RUB"
)

var (
    AvailableCurrencies = []string{CurrencyRUB}
)

Payment methods

const (
  PaymentMethodCard = "card"
  PaymentMethodSBP  = "sbp"
)

var (
  AvailablePaymentMethods = []string{PaymentMethodCard, PaymentMethodSBP}
)
Copyright © 2026