PAYNEX — документация по интеграции для мерчантов

P2P-процессинг депозитов и выплат. Вы (мерчант) создаёте заявки через API, клиент переводит деньги другому клиенту напрямую, а PAYNEX сопоставляет заявки, проводит проверки и присваивает операции статус. Итог приходит вам вебхуком.

Как пользоваться документацией

Задача Куда перейти
Запустить первый запрос за несколько минут Быстрый старт
Разобраться во всём процессе эта страница
Заполнить поля, выполнить запрос и скопировать динамический код Swagger
Выбрать готовый пример на PHP, Python, JavaScript, Go или cURL ReDoc

Merchant API в одной таблице

Что сделать Метод Что считать результатом
Создать Pay-In POST /api/v1/payments payment_id и checkout_url
Проверить Pay-In GET /api/v1/payments/{payment_id} текущий статус депозита
Создать Pay-Out POST /api/v1/payouts payment_id выплаты
Проверить Pay-Out GET /api/v1/payouts/{payout_id} текущий статус выплаты
Проверить расчёты GET /api/v1/balance комиссионный баланс в центах USDT
Посмотреть основной webhook GET /api/v1/webhook сохранённый URL или null
Установить основной webhook PUT /api/v1/webhook новая текущая настройка

Pay-In и Pay-Out имеют отдельные методы статуса, поэтому каждое направление легко отслеживать самостоятельно. Финальный результат обработки приходит единым способом — в статусе операции и подписанном webhook.


1. Авторизация

Передавайте ваш ключ одним из двух способов (что удобнее) — оба равнозначны:

X-API-Key: paynex_live_xxxxxxxxxxxxxxxxxxxx

или

Authorization: Bearer paynex_live_xxxxxxxxxxxxxxxxxxxx

В личном кабинете этот ключ называется Bearer Token. Полное значение показывается ровно один раз — при онбординге или ротации; после этого в ЛК → Настройки доступен только безопасный preview/fingerprint. Если ключ утрачен, перевыпустите его: старый ключ по умолчанию принимается ещё 24 часа (grace period), затем автоматически отзывается. Повторная ротация не накапливает ключи: валидны максимум новый current и один grace key.

Отдельно есть Sign Key (он же api_secret) — он не передаётся в запросах, а нужен только для проверки подписи входящих вебхуков (см. §7). Sign Key также показывается только при выпуске/перевыпуске. Храните оба значения только на сервере и не отправляйте их в URL, логи или клиентский JavaScript.

Ошибки авторизации: 401 (нет/неверный ключ), 403 (мерчант отключён).


2. Создать депозит (Pay-In)

POST /api/v1/payments

Заголовки: X-API-Key, Content-Type: application/json, опц. Idempotency-Key.

Поле Тип Обяз. Описание
amount_kopecks int да сумма в копейках (1..10^9)
currency string нет по умолч. "RUB"; сейчас поддерживается только RUB
payment_method string да "card" или "sbp"
merchant_player_id string да ваш ID клиента (напр. "user_42")
player_full_name string нет ФИО клиента из вашего KYC. Рекомендуется: включает анти-треугольник (сверку с ФИО на чеке)
merchant_payment_id string нет ваш уникальный ID операции; см. правила идемпотентности ниже
return_url string нет абсолютный HTTPS URL возврата на стандартном порту 443
webhook_url string нет Публичный HTTPS URL вебхуков для этого платежа (иначе берётся дефолтный мерчанта)
external_metadata object нет любые ваши данные, вернём их в вебхуках без изменений
expires_in_seconds int нет TTL заявки, 60..21600, по умолч. 1800

Запрос:

curl -X POST https://<домен>/api/v1/payments \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: dep-2025-01-15-0001" \
  -d '{
    "amount_kopecks": 500000,
    "currency": "RUB",
    "payment_method": "sbp",
    "merchant_player_id": "user_42",
    "player_full_name": "Иванов Иван Иванович",
    "merchant_payment_id": "order_98765"
  }'

Ответ 201:

{
  "payment_id": "de09a768-28ff-4b91-920e-3d6c27f4cfb6",
  "status": "matching",
  "direction": "payin",
  "amount_kopecks": 500000,
  "currency": "RUB",
  "payment_method": "sbp",
  "checkout_url": "https://<домен>/pay/Qc6CllN0...",
  "expires_at": "2025-01-15T08:27:28Z",
  "created_at": "2025-01-15T07:57:28Z",
  "merchant_payment_id": "order_98765"
}

Дальше: редиректните клиента на checkout_url. На этой странице он видит реквизиты получателя, переводит деньги и загружает чек. Вам ничего больше вызывать не нужно — финальный статус придёт вебхуком. Итог сверяйте по payment_id (или вашему merchant_payment_id).

⚠️ Сумма может быть округлена под шаг матчинга мерчанта. Тогда в вебхуке будет amount_kopecks (фактическая) и original_amount_kopecks (что вы запросили). Списывайте/ зачисляйте по фактической.


3. Создать выплату (Pay-Out)

POST /api/v1/payouts — здесь обязательны реквизиты получателя.

Общие поля: amount_kopecks, currency (сейчас только RUB), payment_method (card/sbp), merchant_player_id, опц. merchant_payment_id, expires_in_seconds.

Для Pay-Out поле webhook_url сейчас не принимается. События выплаты отправляются только на дефолтный URL мерчанта из ЛК → Настройки. Per-payment webhook_url поддерживается только в POST /api/v1/payments (Pay-In).

Запрос (СБП):

curl -X POST https://<домен>/api/v1/payouts \
  -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: payout-2025-01-15-0001" \
  -d '{
    "amount_kopecks": 500000, "currency": "RUB", "payment_method": "sbp",
    "merchant_player_id": "user_42",
    "merchant_payment_id": "withdrawal_98765",
    "recipient_sbp_phone": "+7 999 123-45-67",
    "recipient_sbp_bank_code": "SBER"
  }'

Ответ 201 содержит payment_id, status, recipient_last4 (для визуальной сверки) и пр. К выплатам применяется тот же шаг округления мерчанта: amount_kopecks в ответе и вебхуке — фактическая сумма, а запрошенная до округления приходит в webhook-поле original_amount_kopecks.


4. Жизненный цикл и статусы

Статус Pay-In запрашивается в разделе платежей:

GET /api/v1/payments/{payment_id}

Статус Pay-Out имеет отдельный метод в разделе выплат:

GET /api/v1/payouts/{payout_id}

В обоих параметрах принимается либо payment_id, который вернул PAYNEX, либо ваш merchant_payment_id. Каждый метод проверяет не только текущего мерчанта, но и направление: Pay-In нельзя получить через payout-метод и наоборот. Чужой, отсутствующий или относящийся к другому направлению ID возвращает одинаковый 404.

curl https://paynex.work/api/v1/payments/order_98765 \
  -H "Authorization: Bearer $KEY"
{
  "payment_id": "de09a768-28ff-4b91-920e-3d6c27f4cfb6",
  "merchant_payment_id": "order_98765",
  "direction": "payin",
  "status": "completed",
  "amount_kopecks": 500000,
  "currency": "RUB",
  "payment_method": "sbp",
  "created_at": "2025-01-15T07:57:28Z",
  "updated_at": "2025-01-15T08:02:10Z",
  "expires_at": "2025-01-15T08:27:28Z",
  "completed_at": "2025-01-15T08:02:10Z"
}

Типичный Pay-In:

pending → matching → awaiting_payment → awaiting_verification → completed
                                          ↘ failed / expired / cancelled

Pay-Out не переходит в awaiting_verification: при N:1 он может возвращаться из awaiting_payment в matching, если отдельная часть освобождена, и завершается только после подтверждения всех частей. Его терминальные неуспешные состояния — failed или expired.

Статус Значение
pending заявка создана
matching ищем встречную заявку в очереди
awaiting_payment пара найдена, ждём перевод от отправителя
awaiting_verification Pay-In: чек загружен, идёт проверка (авто/оператор)
completed успех: перевод подтверждён
failed провал (поддельный чек, отказ оператора и т.п.)
expired истёк таймер ожидания
cancelled Pay-In отменён клиентом со страницы оплаты до загрузки чека

Зачисляйте средства клиенту ТОЛЬКО по completed (приходит вебхуком). Статус awaiting_verification означает, что чек ещё проверяется (см. §8 про антифрод).

Как работает N:1

Один Pay-In не делится между несколькими выплатами. Один Pay-Out может быть собран из нескольких Pay-In того же метода и валюты:

Событие Подтверждено по Pay-Out remaining_kopecks
Создан Pay-Out 5 000 ₽ 0 ₽ 500000
Подтверждён Pay-In 2 500 ₽ 2 500 ₽ 250000
Подтверждён Pay-In 1 500 ₽ 4 000 ₽ 100000
Подтверждён Pay-In 1 000 ₽ 5 000 ₽ 0

Pay-Out становится completed, когда остаток равен нулю и все его части подтверждены. Если отдельный Pay-In отклонён, его сумма возвращается в остаток Pay-Out, а выплата снова может ждать подходящий депозит.


5. Баланс

GET /api/v1/balance

curl https://paynex.work/api/v1/balance \
  -H "Authorization: Bearer $KEY"
{
  "model": "commission_account",
  "currency": "USDT",
  "balance_usdt_cents": 10345,
  "accrued_usdt_cents": 12345,
  "settled_usdt_cents": 2000,
  "accounting_complete": true,
  "unpriced_accruals": 0
}

PAYNEX не хранит кошелёк доступных средств мерчанта. Этот метод возвращает точный комиссионный расчётный баланс: начисленная комиссия минус погашения, в центах USDT. Если часть старой истории нельзя оценить без ручной сверки, accounting_complete=false, а balance_usdt_cents и accrued_usdt_cents возвращаются как null.


6. Idempotency-Key

Для POST /api/v1/payments и POST /api/v1/payouts передавайте заголовок Idempotency-Key (любая ваша уникальная строка на операцию). Повторный запрос с тем же ключом вернёт тот же ранее созданный платёж, без дублирования. Ключ действует в рамках конкретного endpoint: повторяйте тот же метод, путь и JSON-тело. Повтор ключа с другим телом вернёт 409. Используйте ключ при ретраях по таймауту сети и не переиспользуйте его для новой операции.

merchant_payment_id — дополнительный бизнес-инвариант, общий для Pay-In и Pay-Out. Внутри одного мерчанта он уникален во всём пространстве операций: точный повтор нормализованного JSON вернёт уже созданный платёж, а изменённое тело или попытка использовать ID Pay-In для Pay-Out вернёт 409. У разных мерчантов одинаковые значения допустимы. Рекомендуется всегда передавать одновременно и Idempotency-Key, и merchant_payment_id: первый защищает транспортный ретрай endpoint, второй — вашу бизнес-операцию даже при смене транспортного ключа.


7. Вебхуки (как настроить и получать результат)

Основной webhook можно установить прямо через API. Операция идемпотентна:

PUT /api/v1/webhook

curl -X PUT https://paynex.work/api/v1/webhook \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://merchant.example/webhooks/paynex"}'

GET /api/v1/webhook возвращает текущую настройку. Чтобы отключить основной webhook, передайте {"webhook_url":null}. Тот же URL можно задать в ЛК → Настройки.

Для Pay-In основной URL можно переопределить полем webhook_url в POST /api/v1/payments; POST /api/v1/payouts такого поля сейчас не имеет и использует только основной webhook. Если подходящего URL нет в момент события, событие не создаётся, не отправляется и позже автоматически не восстанавливается. Текущий статус всегда можно запросить через GET /api/v1/payments/{payment_id} для Pay-In или GET /api/v1/payouts/{payout_id} для Pay-Out, а также посмотреть в ЛК → Транзакции.

Для перечисленных ниже событий мы шлём POST на ваш webhook URL с JSON-телом и подписью. Webhook создаётся не для каждого промежуточного статуса: например, отдельных событий payment.matching и payment.awaiting_verification сейчас нет. Подпись считается на Sign Key (api_secret).

Webhook endpoint должен использовать https:// и стандартный порт 443. URL с логином/ паролем или fragment (#...) отклоняется при сохранении. Перед каждой доставкой PAYNEX проверяет все полученные DNS-адреса; loopback/private/link-local адрес делает попытку недопустимой. Перенаправления не выполняются: endpoint должен сразу ответить на исходный URL. Время одной попытки и объём читаемого ответа ограничены; возвращайте короткий ответ 2xx.

Заголовки:

Content-Type: application/json; charset=utf-8
X-Paynex-Event-Id:   <уникальный id события>
X-Paynex-Event-Type: payment.completed
X-Paynex-Timestamp:  1781162244
X-Paynex-Signature:  <hex HMAC-SHA256>

Тело:

{
  "event_id": "…",
  "event_type": "payment.completed",
  "created_at": "2025-01-15T08:00:00Z",
  "payment": {
    "id": "de09a768-…", "merchant_payment_id": "order_98765",
    "direction": "payin", "status": "completed",
    "amount_kopecks": 500000, "original_amount_kopecks": null,
    "remaining_kopecks": null, "currency": "RUB", "payment_method": "sbp",
    "external_metadata": {}, "expires_at": "…", "completed_at": "…", "created_at": "…"
  }
}

Типы событий: payment.created, payment.awaiting_payment, payment.completed, payment.failed, payment.expired, payment.cancelled.

Проверка подписи (ОБЯЗАТЕЛЬНО)

Подпись = HMAC_SHA256(api_secret, "{X-Paynex-Timestamp}.{сырое_тело_запроса}"). Сравнивайте с заголовком X-Paynex-Signature. Проверяйте, что timestamp не старше 5 минут (защита от replay). Тело берите как пришло, побайтово (не пересериализуйте).

import hmac, hashlib, time

def verify(api_secret: str, headers: dict, raw_body: bytes) -> bool:
    ts = headers["X-Paynex-Timestamp"]
    if abs(time.time() - int(ts)) > 300:          # старше 5 минут — отклоняем
        return False
    expected = hmac.new(
        api_secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, headers["X-Paynex-Signature"])

Идемпотентность и ретраи


8. Антифрод — что важно знать мерчанту

Чек о переводе технически невозможно на 100% отличить от хорошей подделки по самому файлу. Поэтому решение об одобрении опирается на поведение и историю, а не только на чек:


9. Лимиты и ошибки


10. Быстрые ответы перед запуском

Вопрос Ответ
Когда фиксировать успешный депозит? После completed: этот статус означает готовый финансовый результат.
Как получить результат без webhook? Запросить статус методом нужного направления и проверить журнал в ЛК.
Как работает матчинг N:1? Один Pay-Out можно собрать из нескольких Pay-In; каждый Pay-In используется целиком.
Как начинается поиск пары? Операция сразу переходит в matching и ждёт совместимую заявку в пределах TTL.
Что показывает баланс? Комиссионные начисления за вычетом погашений в центах USDT.
Как настроить URL для событий? Основной URL принимает Pay-In и Pay-Out; отдельный URL можно передать для конкретного Pay-In.

Перед боевым трафиком проверьте успешный Pay-In, Pay-Out, N:1, истечение заявки, повтор HTTP-запроса и повторную доставку webhook. В каждом сценарии сверяйте фактический amount_kopecks, направление и финальный статус.


11. Песочница (dev)