RivoDonate

Оплата на вашем сайте

Вы показываете товары покупателю. RivoDonate принимает оплату через включённые кассы, учитывает комиссии и зачисляет доход магазину. Версия API: v1.

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

  1. Откройте «Магазин → Управление → API интеграция» и создайте ключи.
  2. Сохраните их на сервере своего сайта. Укажите HTTPS адрес webhook и страницы возврата.
  3. Отправьте POST запрос с уникальным external_order_id.
  4. Перенаправьте покупателя на checkout_url. Там он выберет способ оплаты.
  5. Подтвердите заказ по подписанному событию order.paid или запросом статуса.

Ключ нельзя помещать в JavaScript браузера, мобильное приложение или публичный репозиторий. У каждого магазина свой ключ; заменить его можно только после удаления прежнего.

Адрес API

https://rivodonate.fun/api/v1/orders

Если сервер не поддерживает красивые адреса, тот же обработчик доступен по /index.php?route=merchant_orders. Запросы только с сервера; CORS для браузерной работы с ключом не включён.

Создание внешнего заказа

POST /api/v1/orders · Authorization: Bearer YOUR_API_KEY · Content-Type: application/json

curl -X POST 'https://rivodonate.fun/api/v1/orders' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  --data '{
    "external_order_id": "order-1001",
    "title": "Привилегия VIP",
    "amount": "499.00",
    "currency": "RUB",
    "player_nick": "Steve",
    "customer_email": "player@example.com"
}'
ПолеФормат и назначение
external_order_idОбязательно. Уникальный ID заказа в вашем магазине, 1-100 символов: A-Z, a-z, 0-9, _, -, ., :.
titleДля внешнего товара: название до 150 символов.
amountДля внешнего товара: строка в RUB, до двух знаков после точки. Минимум задаёт платформа, максимум 1 000 000.
currencyНеобязательно. Только RUB. USDT в кассе не меняет валюту заказа API.
player_nickОбязательно. Ник игрока, 3-16 символов: латиница, цифры, _.
customer_emailОбязательно. Email покупателя для чека.

Сумму определяет ваш сервер по своему каталогу. Не доверяйте сумме, полученной от браузера покупателя. Комиссия рассчитывается RivoDonate: base_amount - цена товара, amount - сумма с долей комиссии покупателя. При отдельных комиссиях до выбора способа оплаты эта сумма предварительная. После выбора она пересчитывается по ставке способа и фиксируется при создании счёта в кассе. В подтверждении оплаты приходит окончательная сумма. Не передавайте комиссию повторно.

Ответ: 201 Created

{
    "data": {
        "id": "0123456789abcdef0123456789abcdef0123456789abcdef",
        "external_order_id": "order-1001",
        "status": "pending",
        "title": "Привилегия VIP",
        "amount": "548.90",
        "base_amount": "499.00",
        "currency": "RUB",
        "payment_method": "",
        "checkout_url": "https://rivodonate.fun/index.php?route=api_checkout&order=0123456789abcdef0123456789abcdef0123456789abcdef",
        "expires_at": "2026-10-16T12:00:00+00:00",
        "paid_at": null
    }
}

Сумма и дата в примере условные. Используйте значения из фактического ответа.

Повтор запроса без двойного заказа

Повторите запрос с тем же external_order_id и теми же полями: API вернёт прежний заказ с HTTP 200. Изменённые данные с этим ID дают 409. После сетевого таймаута повторяйте прежний запрос; не генерируйте новый ID, пока не выяснен статус старого. ID уникален в пределах магазина и сохраняется после смены ключей.

Пример PHP

Переменные окружения: RIVO_API_BASE - адрес платформы, RIVO_API_KEY - ваш ключ. Данные заказа берите из своей серверной базы.

$payload = [
    'external_order_id' => 'order-1001',
    'title' => 'Привилегия VIP',
    'amount' => '499.00',
    'currency' => 'RUB',
    'player_nick' => 'Steve',
    'customer_email' => 'player@example.com'
];
$curl = curl_init(getenv('RIVO_API_BASE') . '/api/v1/orders');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 20,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RIVO_API_KEY'),
        'Content-Type: application/json'
    ],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR)
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($body === false || !in_array($status, [200, 201], true)) {
    throw new RuntimeException('Не удалось создать заказ');
}
$order = json_decode($body, true, 512, JSON_THROW_ON_ERROR)['data'];
header('Location: ' . $order['checkout_url'], true, 303);
exit;

Товар из каталога RivoDonate

Передайте product_id вместо title и amount. Цена, скидки, комиссия и команды выдачи берутся из RivoDonate; подменить их через API нельзя. Товар должен принадлежать магазину ключа и быть включён.

{
    "external_order_id": "order-1002",
    "product_id": 42,
    "server_id": 7,
    "player_nick": "Steve",
    "customer_email": "player@example.com"
}
  • server_id - целый ID привязанного сервера. При отсутствии выбирается первый привязанный сервер по ID.
  • promo_code - необязательный действующий промокод.
  • resource_amount - числовое количество для товара с переменным количеством. Для обычного товара не передаётся.
  • ID товара и сервера можно посмотреть в панели магазина при редактировании.

Для внешнего товара выдачу выполняет ваш сервер по webhook. Команды, цена и название фиксируются при создании заказа; изменения каталога не меняют существующую оплату.

Статусы и страницы возврата

curl 'https://rivodonate.fun/api/v1/orders?id=ORDER_ID' \
  -H 'Authorization: Bearer YOUR_API_KEY'

GET возвращает тот же объект data. Доступны только заказы магазина текущего ключа. Запрос читает локально подтверждённый статус; не инициирует новый платёж.

СтатусЧто делать
pendingОплата ещё не подтверждена. Не выдавать товар.
paidОплата подтверждена. Выдать внешний товар один раз.
failed, cancelled, expiredОплата не завершена. Не выдавать товар.
underpaidКасса сообщила о недоплате. Не выдавать товар.

Неоплаченная ссылка действует 24 часа до начала оплаты в кассе. После создания счёта срок и результат определяет касса. Заказ может перейти в paid после позднего подтверждения даже после неуспешного события - paid имеет приоритет.

Ссылки успешной и неудачной оплаты сохраняются из настроек магазина при создании заказа. Возврат из кассы направляет на них только после подтверждённого соответствующего статуса. Пока статус pending, покупатель видит ожидание и кнопку обновления. Наличие покупателя на вашей странице успеха не доказывает оплату.

Подписанные уведомления

RivoDonate отправляет JSON POST на сохранённый Webhook URL. Внешние товары требуют настроенного webhook. HTTPS, публичный домен и доступный публичный IPv4 обязательны. Переадресации не выполняются.

{
    "event_id": "evt_123",
    "event": "order.paid",
    "data": {
        "id": "ORDER_ID",
        "external_order_id": "order-1001",
        "status": "paid",
        "title": "Привилегия VIP",
        "amount": "548.90",
        "base_amount": "499.00",
        "currency": "RUB",
        "payment_method": "banka_sbp",
        "expires_at": "2026-10-16T12:00:00+00:00",
        "paid_at": "2026-10-15T12:05:00+00:00"
    }
}

Заголовки: X-Rivo-Event-Id, X-Rivo-Timestamp (Unix UTC) и X-Rivo-Signature. Подпись - шестнадцатеричный HMAC-SHA256 от timestamp + "." + исходное тело запроса с вашим Webhook-секретом. Проверяйте подпись до разбора и обработки события; допускайте расхождение часов до 5 минут.

Приём с устойчивым сохранением (PHP + SQLite)

Один раз создайте таблицу входящих событий. Возвращайте 2xx только после сохранения события. Отдельный обработчик читает очередь и выдаёт заказ в транзакции, проверяя его ID, сумму, валюту и статус paid. Пример ниже сохраняет событие; логику выдачи вашего товара добавляет ваш сайт.

CREATE TABLE rivo_inbox (
    event_id TEXT PRIMARY KEY,
    payload TEXT NOT NULL,
    processed_at TEXT
);
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_RIVO_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_RIVO_SIGNATURE'] ?? '';
$secret = getenv('RIVO_WEBHOOK_SECRET');
if (!$secret || !ctype_digit($timestamp) || abs(time() - (int)$timestamp) > 300) {
    http_response_code(401);
    exit;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true, 16, JSON_THROW_ON_ERROR);
$pdo = new PDO(getenv('DATABASE_DSN'));
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->beginTransaction();
$insert = $pdo->prepare('INSERT INTO rivo_inbox(event_id, payload) VALUES (?, ?) ON CONFLICT(event_id) DO NOTHING');
$insert->execute([$event['event_id'], $raw]);
$pdo->commit();
http_response_code(204);

Повторы используют одинаковые event_id и тело, но новый timestamp и подпись. Уникальность выдачи обеспечивайте по ID заказа, а не только по событию. Не переводите уже оплаченный заказ обратно в failed при запоздавшем событии.

Любой HTTP 2xx означает приём. При ошибке выполняется до 10 попыток с увеличением интервала от 1 минуты до 6 часов. Итог доставки виден в настройках интеграции; после исчерпания попыток доступна кнопка повтора. При удалении ключей старые заказы используют прежний webhook-секрет - сохраните его до окончания обработки старых заказов.

Ошибки и ограничения

{"error":"validation_error","message":"Некорректный customer_email."}
HTTPПричина
401Ключ не передан, удалён или неверен.
403API выключен, магазин недоступен, включён тестовый режим или требуется верификация.
404Заказ не найден в магазине ключа.
409external_order_id использован с другими данными.
413 / 415Тело больше 16 КиБ / неверный Content-Type.
422Некорректные или неизвестные поля.
429Лимит: 60 запросов в минуту на магазин, 120 на IP. Учитывайте Retry-After.
500Внутренняя ошибка. Повторите запрос с прежним external_order_id.

Настройка сервера

Предпросмотр формы в панели не создаёт заказ и не списывает деньги. Реальные API оплаты доступны при выключенном тестовом режиме магазина и включённых кассах. Кнопка оплаты создаёт только один счёт; повторное нажатие не выбирает другую кассу и не создаёт дубликат. При неопределённом ответе кассы сначала проверьте статус.

Очередь webhook обрабатывается штатным app/notification_worker.php. На сервере должен работать его cron раз в минуту. Если он уже настроен, второй обработчик добавлять не нужно. Для отдельного cron:

* * * * * /usr/bin/php /path/to/site/app/merchant_webhook_worker.php

Требуются PHP 8.1+, cURL, OpenSSL, mbstring, PDO SQLite и доверенные CA сертификаты. При резервном копировании сохраняйте базу вместе с storage/merchant-api.key: без этого файла нельзя расшифровать секреты старых webhook. API ключ хранится только как хеш; показать его повторно невозможно.