Voyage Pay / Merchant Gateway

Merchant Gateway

Собственный API Voyage Pay для создания ордеров, получения статуса и приёма подписанных событий.

SECTION / 01

Быстрый старт

От ключа до checkout URL за четыре шага.

01

Получите Voyage Key

Овнер создаёт merchant workspace. Ключ начинается с vp_live_ и хранится только на вашем backend.
02

Отправьте order request

Передайте свой external_id, сумму, метод и URL для событий.
03

Откройте checkout_url

Перенаправьте клиента на URL из order envelope.
04

Примите order.paid

Проверьте Voyage-Signature, зафиксируйте delivery_id и ответьте HTTP 2xx.
SECTION / 02

Доступ к API

Base URLhttps://api.voyagepay.proЕдиный production-адрес Merchant Gateway.
Voyage-Api-Keyvp_live_…Обязателен для создания и проверки ордера.
Webhook secretvp_wh_…Используется для HMAC-подписи событий.

Граница доверия

Не передавайте Voyage Key в browser, mobile application, URL, логи или аналитику.
SECTION / 03

Новый ордер

POST /api/merchant/v1/orders

curl -X POST https://api.voyagepay.pro/api/merchant/v1/orders \
  -H "Content-Type: application/json" \
  -H "Voyage-Api-Key: vp_live_ВАШ_КЛЮЧ" \
  -d '{
    "external_id": "checkout-10492",
    "amount": 10000,
    "currency": "RUB",
    "payment_method": "sbp",
    "traffic_category": "classic",
    "operation": "payin",
    "notification_url": "https://merchant.example/events/voyage",
    "redirect_url": "https://merchant.example/checkout/complete",
    "cancel_redirect_url": "https://merchant.example/checkout/cancelled",
    "note": "Заказ 10492"
  }'
external_idstringВаш уникальный ID заказа и ключ идемпотентности.
amountnumber ≥ 1Сумма ордера.
currencyRUBРабочая валюта.
payment_methodcard | sbpКарта или СБП.
traffic_categoryclassic | bt | mkКатегория маршрутизации; classic по умолчанию.
operationpayin | payoutНаправление; payin по умолчанию.
notification_urlHTTPS URLАдрес для Voyage Events.

Безопасные повторы

Повторите исходный запрос с тем же external_id, если не получили ответ из-за сетевого сбоя. Voyage Pay вернёт созданный ранее ордер. Если остальные параметры отличаются, API ответит 409 Conflict.
SECTION / 04

Ответ и статус

{
  "request_id": "8c998946-93ef-4bcc-b119-1b874ac69c42",
  "order": {
    "id": "8c998946-93ef-4bcc-b119-1b874ac69c42",
    "external_id": "checkout-10492",
    "state": "awaiting_payment",
    "amount": { "value": 10000, "currency": "RUB" },
    "payment_method": "sbp",
    "traffic_category": "classic",
    "operation": "payin",
    "expires_at": "2026-07-21T12:30:00.000Z",
    "checkout_url": "https://voyagepay.pro/pay/8c998946-93ef-4bcc-b119-1b874ac69c42",
    "payment_details": {
      "bank": "bank-name",
      "recipient": "Получатель",
      "account": "+7 900 000-00-00"
    }
  }
}

GET /api/merchant/v1/orders/:id с заголовком Voyage-Api-Key.

awaiting_paymentОжидает оплатыОрдер активен.
paidОплаченФинальный положительный статус.
declinedОтклонёнОрдер не завершён.
expiredИстёкВремя оплаты закончилось.
cancelledОтменёнОрдер отменён.
SECTION / 05

Диспуты

Откройте спор только по собственному ордеру и отслеживайте решение по API.

POST /api/merchant/v1/disputes

curl -X POST https://api.voyagepay.pro/api/merchant/v1/disputes \
  -H "Content-Type: application/json" \
  -H "Voyage-Api-Key: vp_live_ВАШ_КЛЮЧ" \
  -d '{
    "order_id": "8c998946-93ef-4bcc-b119-1b874ac69c42",
    "reason": "not_received",
    "description": "Клиент сообщил об оплате, но ордер не подтверждён",
    "evidence": [
      "https://merchant.example/evidence/receipt-10492.jpg"
    ]
  }'
order_idUUIDID ордера Voyage Pay. Чужой ордер недоступен.
reasonenumnot_received | wrong_amount | operator_inactive | other
descriptionstringОписание ситуации: от 5 до 2000 символов.
evidenceHTTPS URL[]Необязательно: до 10 защищённых ссылок на доказательства.
{
  "request_id": "54f9b753-5af0-44e9-9e6a-1c3c6a8838b8",
  "dispute": {
    "id": "54f9b753-5af0-44e9-9e6a-1c3c6a8838b8",
    "order_id": "8c998946-93ef-4bcc-b119-1b874ac69c42",
    "status": "open",
    "reason": "not_received",
    "description": "Клиент сообщил об оплате, но ордер не подтверждён",
    "evidence": ["https://merchant.example/evidence/receipt-10492.jpg"],
    "operator_response": null,
    "operator_evidence": [],
    "resolution_note": null,
    "created_at": "2026-07-31T01:20:00.000Z",
    "updated_at": "2026-07-31T01:20:00.000Z",
    "resolved_at": null
  }
}
GET /api/merchant/v1/disputesСписокПоследние 100 диспутов текущего мерчанта.
GET /api/merchant/v1/disputes/:idКарточкаТекущий статус, ответ оператора и решение.
openОткрытДиспут зарегистрирован.
under_reviewНа рассмотренииОператор ответил, администрация проверяет материалы.
resolved_merchantВ пользу мерчантаРешение принято в пользу мерчанта.
resolved_operatorВ пользу оператораОрдер подтверждён и проведён.
cancelledОтменёнДиспут отменён инициатором.

Когда открывать диспут

Диспут можно открыть по активному, истёкшему или уже автоматически отменённому неподтверждённому ордеру. Если первоначальный резерв уже вернулся оператору, Voyage Pay повторно удержит доступную сумму из рабочего и страхового депозитов, а недостающую часть зафиксирует как долг оператора. Нельзя открыть диспут только по уже подтверждённому ордеру. Для одного ордера одновременно допускается один активный диспут. Все запросы требуют заголовок Voyage-Api-Key.
SECTION / 06

Voyage Events

POST https://merchant.example/events/voyage
Voyage-Delivery-Id: 4b811e8c-4db7-49a7-a762-662f77a1f88c
Voyage-Event: order.paid
Voyage-Signature: v1=<hex_hmac>

{
  "delivery_id": "4b811e8c-4db7-49a7-a762-662f77a1f88c",
  "type": "order.paid",
  "occurred_at": "2026-07-21T12:15:00.000Z",
  "data": {
    "order_id": "8c998946-93ef-4bcc-b119-1b874ac69c42",
    "external_id": "checkout-10492",
    "state": "paid",
    "amount": { "value": 10000, "currency": "RUB" },
    "payment_method": "sbp",
    "traffic_category": "classic",
    "fee": 250
  }
}

Подпись считается от точных raw bytes HTTP body. Не собирайте JSON заново перед проверкой.

import crypto from 'node:crypto';

const calculated = 'v1=' + crypto
  .createHmac('sha256', process.env.VOYAGE_WEBHOOK_SECRET)
  .update(rawRequestBody)
  .digest('hex');

const valid = crypto.timingSafeEqual(
  Buffer.from(request.headers['voyage-signature']),
  Buffer.from(calculated),
);

Повторы

Voyage Pay повторяет неуспешные доставки. Дедуплицируйте по delivery_id.
SECTION / 07

Чек-лист запуска

01

Проверяйте подпись

Отклоняйте событие до любой бизнес-логики, если Voyage-Signature неверна.
02

Храните delivery_id

Повторная доставка не должна повторно менять заказ.
03

Доверяйте state

redirect_url не подтверждает оплату. Источник истины — order.paid и GET order.
04

Отвечайте 2xx

Отправляйте 2xx только после надёжной фиксации события.