HTTP API
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 POST | credentialsWebhookUrl | Webhook для предоставления реквизитов |
| Webhook POST | statusWebhookUrl | Webhook для уведомления об изменении статуса |
Заголовки безопасности и подписи
Все взаимодействия с API требуют использования криптографической подписи.
Запросы клиента к API
Каждый запрос должен содержать следующие заголовки:
X-KeyId— идентификатор ключа, полученный при создании мерчантаX-Signature— подпись тела запроса, полученная с помощью приватного ключа
Внимание: значения
X-KeyIdиX-Signature, используемые клиентом и сервером, всегда различаются, так как используются разные ключи.
Ответы сервера
Ответы сервера также подписываются:
- Заголовки
X-KeyIdиX-Signatureприсутствуют в ответе. - Подпись (
X-Signature) создается на основе тела ответа, закодирована в base64 и может быть проверена публичным ключом сервера.
Webhook-запросы от сервера
Webhook-запросы также подписываются нашим приватным ключом. Вы можете проверять их с помощью предоставленного нами публичного ключа.
Алгоритм и генерация ключей
- Алгоритм: ed25519
- Публичный ключ (в base64) передаётся на сервер вручную
- Не используйте
opensslдля генерации ed25519, т.к. он добавляет лишние метаданные - Рекомендуется использовать библиотеку соответствующего языка
Пример генерации пары ключей ed25519
Код ниже сохраните как main.go и запустите локально (go run main.go). Он создаёт в текущей папке два файла: private.key (приватный ключ, 64 байта в base64 — хранится у вас) и public.key (публичный ключ, 32 байта в base64 — передаётся нам). Существующие файлы будут перезаписаны, поэтому запускайте в пустой папке.
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"os"
)
func main() {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
if err := os.WriteFile("private.key", []byte(base64.StdEncoding.EncodeToString(priv)), 0o600); err != nil {
panic(err)
}
if err := os.WriteFile("public.key", []byte(base64.StdEncoding.EncodeToString(pub)), 0o644); err != nil {
panic(err)
}
}
Такие же скрипты для Node.js, Python и PHP описаны на странице Ключи для Payin.
Никогда не генерируйте ключи в онлайн-песочницах (Go Playground и аналоги). Такие сервисы кэшируют результат по исходному коду: одинаковый сниппет возвращает всем один и тот же приватный ключ, и подделать вашу подпись сможет любой, кто запустил тот же код. Генерируйте ключи только локально.
Примеры формирования подписи
Файл private.key содержит ключ в base64, поэтому перед подписью его нужно раскодировать.
Go
import (
"crypto/ed25519"
"encoding/base64"
"os"
"strings"
)
func signMessage(msg []byte, privateKeyPath string) (string, error) {
raw, err := os.ReadFile(privateKeyPath)
if err != nil {
return "", err
}
privKeyBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(raw)))
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") as f:
raw = base64.b64decode(f.read().strip())
sk = SigningKey(raw[:32])
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 fs = require('fs');
const privateKey = Buffer.from(fs.readFileSync('private.key', 'utf8').trim(), 'base64');
const message = Buffer.from(JSON.stringify({ example: 'data' }));
const signature = nacl.sign.detached(message, new Uint8Array(privateKey));
const signatureBase64 = Buffer.from(signature).toString('base64');
// 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],
"otherAmountsPossible": true
}
availableAmounts— суммы, под которые встречное предложение есть прямо сейчас. Список ничего не резервирует: между/availableи/newсумму может занять другой платёж.otherAmountsPossible— перечень неполный: платежи на суммы вне списка тоже могут пройти. ПустойavailableAmountsприotherAmountsPossible: trueне означает отказ — заявку стоит создать. Приfalseсписок исчерпывающий.
Ошибка
{
"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", // Опционально (см. примечание ниже). Код банка плательщика (отправителя)
"merchantId": "your-internal-id" // Опционально. Ваш идентификатор; только для сверки и поддержки.
}
- Опциональные поля:
statusWebhookUrl,payerBank,merchantId. Остальные — обязательные. - Поле
payerBankможет быть обязательным по условиям работ. - Поле
merchantId— ваш произвольный идентификатор. Мы его принимаем и сохраняем, но используем только для сверки и общения с поддержкой; на подбор реквизитов и обработку платежа он не влияет.
Успешный ответ
{
"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", // Опционально (см. примечание ниже). Код банка плательщика (отправителя)
"merchantId": "your-internal-id" // Опционально. Ваш идентификатор; только для сверки и поддержки.
}
- Опциональные поля:
statusWebhookUrl,payerBank,merchantId. Остальные — обязательные. - Поле
payerBankможет быть обязательным по условиям работ. - Поле
merchantId— ваш произвольный идентификатор. Мы его принимаем и сохраняем, но используем только для сверки и общения с поддержкой; на подбор реквизитов и обработку платежа он не влияет.
Успешный ответ
{
"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— сервер возвращает HTTP403с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",
"sum": 100.5,
"payeeAccount": "1234567890",
"payeeAccountBank": "Bank",
"payeeName": "HolderName",
"dealSum": 90,
"dealCurrency": "RUB",
"dealRate": 0.9,
"deeplink": "https://deeplink.example.com/frame/abc123"
}
Обратите внимание: через данный вебхук могут быть переданы только следующие статусы:
receiptRequired,doneSuccess,doneFail,arbitrageCancelledByMerchant
Поля с пустым или нулевым значением в запросе не передаются.
Разница между sum и dealSum
sum— фактически оплаченная сумма в валюте платежа. Если клиент оплатил больше или меньше, здесь придёт итоговая изменённая сумма.dealSum— сумма сделки в валюте баланса мерчанта (dealCurrency), рассчитанная по курсуdealRateс учётом комиссии в момент выдачи реквизитов. При изменении фактически оплаченной суммы не пересчитывается: приходит то же значение, что было передано в вебхукеcredentialsWebhookUrl.
Чтобы получить фактическую сумму в валюте баланса, пересчитайте её самостоятельно: sum * dealRate для рублёвого баланса и sum / dealRate для баланса в USDT (где dealRate — курс USDT/RUB).
POST /receipt/attach
Описание: Прикрепление чека.
Content-Type: multipart/form-data
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
orderId | string | да | Идентификатор заказа |
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}
)