API Ludovex

Подключите каталог, кошелёк и заказы через REST и JSON. Баланс и цены заказов указаны в USDT; произвольная сумма кошелька Steam поддерживает USD, RUB, KZT и UAH.

OpenAPI 3.1 JSONПолучить API-ключhttps://api.ludovex.org/api/v2

Аутентификация и разрешения

Передавайте X-API-Key в каждом защищённом запросе. Создайте ключ в Кабинет → API-ключи; полное значение показывается только один раз.

  • readЧтение профиля, каталога и заказов.
  • orderСоздание заказов. Добавьте read отдельно, если этот ключ также проверяет их статус.
  • balanceЧтение баланса кошелька. Одного разрешения read для этого недостаточно.

Храните ключи на сервере, не помещайте их в код браузера или журналы. По возможности ограничьте ключ внешними IP/CIDR вашего сервера; пустой список разрешает любой IP.

Примеры читают LUDOVEX_API_KEY из окружения сервера. В LUDOVEX_GAME_KEY и LUDOVEX_VARIATION_ID укажите значения из каталога, а в LUDOVEX_ORDER_KEY — уникальный идентификатор, сохранённый для одного логического заказа. Для проверки статуса запишите полученный ID заказа в LUDOVEX_ORDER_ID.

Пример
curl --fail-with-body "https://api.ludovex.org/api/v2/me" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"

От каталога до подтверждённой доставки

Уточните обязательные поля товара до отправки платного заказа.

  1. GET /catalog возвращает доступные предложения игр. Сохраняйте game_key без изменений: идентификатор может быть непрозрачным. Не составляйте его из названия товара.
  2. GET /catalog/{gameKey} возвращает текущие предложения. Выберите полученный variation_id, проверьте доступность и обязательные поля; сохраняйте идентификаторы category/region без изменений.
  3. POST /order создаёт заказ с этими идентификаторами и заранее сохранённым ключом идемпотентности. Передавайте player_id, zone_id или provider_inputs только по требованиям выбранного товара. Не помещайте пароли в player_id или zone_id; если каталог явно требует учётные данные, передавайте их только в объявленных полях provider_inputs и не записывайте в журналы.
  4. HTTP 201 подтверждает создание, а не доставку. Проверяйте status: для PROCESSING нужен последующий GET /order/{id} или проверенный webhook. Только COMPLETED означает выполнение; FAILED и REFUNDED обрабатывайте отдельно.

Прежние идентификаторы принимаются для совместимости. В новых интеграциях используйте точные значения из ответов Ludovex. Запрос цены по каталогу не фиксирует цену.

Частота запросов и дневные квоты

Для API-ключа действует лимит 60 запросов в минуту. POST /order дополнительно ограничен 30 запросами в минуту; запросы заказов учитываются в обоих лимитах.

Учитывайте X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset, когда они возвращаются. При RATE_LIMITED дождитесь срока Retry-After; не повторяйте платный заказ с новым ключом идемпотентности.

Дневная квота заказов общая для всех API-ключей аккаунта и зависит от настроек тарифа. DAILY_LIMIT_EXCEEDED означает исчерпание этой квоты, а не минутного лимита. Отсчёт обновляется в полночь Asia/Tashkent. GET /me возвращает профиль и тариф, но не текущее дневное использование или поля времени сброса.

Ошибки

Формат ответа: { ok, data } или { ok: false, error: { code, message } }. Принимайте решения по error.code, а не по переведённому тексту сообщения.

КодHTTPОписание
UNAUTHORIZED401API-ключ отсутствует, неверен или отозван.
FORBIDDEN_SCOPE403У ключа нет требуемого разрешения.
IP_NOT_ALLOWED403Ключ действителен, но исходящий IP запроса не разрешён.
NOT_FOUND404Ресурс не найден или не принадлежит этому аккаунту.
VALIDATION_FAILED422Неверные поля запроса. Исправьте их перед повтором.
IDEMPOTENCY_REQUIRED422Передайте idempotency_key в теле или заголовок Idempotency-Key.
INVALID_ORDER422Неверный товар, сумма, входные данные или заказ. Прочитайте сообщение и исправьте запрос.
INSUFFICIENT_BALANCE402Недостаточно средств в кошельке.
RATE_LIMITED429Достигнут минутный лимит. Учитывайте Retry-After.
DAILY_LIMIT_EXCEEDED429Дневная квота тарифа исчерпана. Повторите после её обновления или смены тарифа.

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

До отправки сохраните один ключ идемпотентности вместе с точным запросом заказа. При тайм-ауте повторите тот же запрос и ключ; не создавайте новый ключ для восстановления неизвестного результата.

Для нового логического заказа или изменённых данных используйте новый ключ. Повтор ключа не редактирует существующий заказ. Сохраняйте полученный ID заказа для проверки статуса и сверки.

Эндпоинты

https://api.ludovex.org/api/v2

GET/api/v2/meРазрешение: read

Поля владельца ключа: id, email, name, status и plan.

curl --fail-with-body "https://api.ludovex.org/api/v2/me" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"
GET/api/v2/balanceРазрешение: balance

Текущий баланс кошелька и валюта (USDT); требуется разрешение balance.

curl --fail-with-body "https://api.ludovex.org/api/v2/balance" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"
GET/api/v2/catalogРазрешение: read

Текущий каталог игр. Сохраняйте каждый полученный game_key.

curl --fail-with-body "https://api.ludovex.org/api/v2/catalog" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"
GET/api/v2/catalog/{gameKey}Разрешение: read

Предложения одной игры с ценами для вашего аккаунта. Необязательный category выбирает полученный идентификатор региона.

  • gameKeyОбязательноТочный game_key из каталога; кодируйте его как компонент пути URL.
  • categoryНеобязательноНеобязательный идентификатор category/region из каталога; используйте URL-кодирование.
curl --fail-with-body "https://api.ludovex.org/api/v2/catalog/$LUDOVEX_GAME_KEY" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"
POST/api/v2/orderРазрешение: order

Создание платного заказа. После HTTP 201 статус может оставаться PROCESSING. Эти примеры предназначены для обычного товара с variation; произвольные суммы Steam описаны ниже.

  • game_keyОбязательноТочный game_key из каталога; кодируйте его как компонент пути URL.
  • variation_idПри необходимостиОбязателен для обычных товаров. Необязателен для Steam с amount + currency или старым amount_usd; если указан, должен совпадать с расчётом.
  • amountПри необходимостиЗапрошенная сумма кошелька Steam десятичной строкой, до 2 знаков после точки. Читайте min/max/step выбранной валюты из wallet_currencies. Передавайте вместе с currency, без старых полей суммы.
  • currencyПри необходимостиВалюта кошелька Steam: USD, RUB, KZT или UAH. Обязательна вместе с amount. База курсов — 1 USD; итоговое списание остаётся в USDT.
  • expected_price_usdtНеобязательноЗначение price_usdt из расчёта Steam, точной строкой с 8 знаками после точки. Рекомендуется для фиксации принятой цены; изменение возвращает STEAM_QUOTE_CHANGED до списания. При неоднозначном тайм-ауте повторяйте исходный ключ идемпотентности.
  • amount_usdПри необходимостиСтарый вариант суммы Steam в USD десятичной строкой, например "10.25". Читайте min_usd/max_usd/step из custom_amount. Не сочетайте с amount + currency или custom_amount_uzs.
  • player_idПри необходимостиID или логин игрока, если этого требует выбранный товар. Не обязателен для каждого кода или подарочной карты.
  • zone_idПри необходимостиСервер/зона, если требуется выбранным товаром.
  • provider_inputsПри необходимостиДополнительные поля, явно объявленные схемой ввода товара; учётные данные передаются только при таком требовании схемы. Отправляйте объект с точными именами полей; не записывайте учётные данные в журналы или в player_id/zone_id.
  • idempotency_keyПри необходимостиСохранённый уникальный ключ логического заказа либо заголовок Idempotency-Key.
python3 - <<'PY' > order.json
import json, os
body = {
    "game_key": os.environ["LUDOVEX_GAME_KEY"],
    "variation_id": os.environ["LUDOVEX_VARIATION_ID"],
}
for field in ("player_id", "zone_id"):
    if os.environ.get(field.upper()):
        body[field] = os.environ[field.upper()]
if os.environ.get("PROVIDER_INPUTS_JSON"):
    body["provider_inputs"] = json.loads(os.environ["PROVIDER_INPUTS_JSON"])
print(json.dumps(body))
PY
curl --fail-with-body -X POST "https://api.ludovex.org/api/v2/order" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Idempotency-Key: $LUDOVEX_ORDER_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @order.json
GET/api/v2/order/{id}Разрешение: read

Текущий статус и доставленные данные заказа, принадлежащего этому аккаунту.

  • idОбязательноID, полученный при создании заказа; целочисленный параметр пути.
curl --fail-with-body "https://api.ludovex.org/api/v2/order/$LUDOVEX_ORDER_ID" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"
GET/api/v2/ordersРазрешение: read

История заказов по убыванию ID. Без page или before_id сохраняется прежний ответ-массив (по умолчанию 50, максимум 100 записей).

  • limitНеобязательноКоличество новых строк от 1 до 100; по умолчанию 50.
  • pageНеобязательноВключает объект с пагинацией: страницы 1–1000. Для глубокой истории используйте before_id.
  • before_idНеобязательноИсключающий курсор по убыванию ID, полученный как next_cursor. page должен отсутствовать или равняться 1.
  • snapshot_idНеобязательноПовторяйте snapshot_id из первого ответа, чтобы новые заказы не сдвигали последующие страницы.
  • created_fromНеобязательноНачальная дата создания включительно, YYYY-MM-DD, Asia/Tashkent (UTC+5).
  • created_toНеобязательноКонечная дата создания включительно, YYYY-MM-DD, Asia/Tashkent (UTC+5).
  • updated_sinceНеобязательноВремя обновления включительно в RFC3339, например 2026-10-21T00:00:00Z. Перекрывайте интервалы и удаляйте дубликаты.
  • statusНеобязательноPROCESSING, COMPLETED, FAILED или REFUNDED. Фильтр учитывает флаг возврата средств.

Для полной истории начните с page=1. Читайте data.items, data.has_more, data.next_cursor и data.snapshot_id. Продолжайте с before_id=next_cursor, сохраняя snapshot_id и фильтры, пока has_more=false. Не сочетайте before_id с page>1. Snapshot ограничивает новые ID, но существующие заказы могут меняться: удаляйте повторы по ID и используйте перекрывающиеся окна updated_since.

Объект также содержит total, page и limit. Записи в пагинации включают updated_at и refunded. Экспорт аккаунта доступен на страницах заказов и транзакций. Для слишком больших экспортов сократите период.

curl --fail-with-body "https://api.ludovex.org/api/v2/orders?page=1&limit=20" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Accept: application/json"

Steam: USD, RUB, KZT и UAH

Выберите валюту кошелька Steam. С баланса Ludovex списывается USDT с учётом рассчитанной скидки Steam 3.5%.

  1. Найдите Steam через GET /api/v2/catalog и сохраните game_key. В деталях каталога прочитайте custom_amount, exchange_rates и wallet_currencies; нужен scope read.
  2. GET /api/v2/catalog/{gameKey}?amount=1000.00&currency=RUB возвращает массив data с одним расчётом. Без суммы возвращаются минимальный расчёт в USD и текущие параметры валют. Заказ и списание не создаются.
  3. В POST /api/v2/order передайте те же game_key, amount и currency, логин Steam в player_id, принятую price_usdt в expected_price_usdt и сохранённый ключ идемпотентности. Нужен scope order.

В примерах задайте STEAM_GAME_KEY или LUDOVEX_STEAM_GAME_KEY равным идентификатору lvxa_g_… из каталога. Не формируйте его самостоятельно. Старый ключ steam-topup также принимается.

1. Проверка суммы и расчёт цены — scope read
# STEAM_GAME_KEY = game_key returned by GET /catalog
# Example wallet amount: 1000.00 RUB. Also supported: USD, KZT, UAH.
curl --get "https://api.ludovex.org/api/v2/catalog/$STEAM_GAME_KEY" \
  --data-urlencode "amount=1000.00" --data-urlencode "currency=RUB" \
  -H "X-API-Key: $LUDOVEX_API_KEY"
amount / wallet_currency
amount — сумма пополнения, wallet_currency — её валюта в расчёте. В запросе используется currency: USD, RUB, KZT или UAH. Передавайте JSON-строку, например "1000.00", не более чем с 2 знаками после точки.
price_usdt / currency
price_usdt — точная сумма списания USDT строкой с 8 знаками после точки; currency — USDT. Сохраняйте строку без изменений. amount_usd_equivalent — справочный эквивалент USD, а не сумма списания USDT.
exchange_rates / wallet_currencies
exchange_rates.base — USD: курс показывает сумму валюты кошелька за 1 USD, не за USDT. Используйте updated_at и выбранный exchange_rate; не подставляйте выдуманный курс при недоступности.
nominal_usd / estimated_wallet_amount
Для RUB, KZT и UAH nominal_usd округляет amount / rate вверх до 2 знаков перед скидкой 3.5%. estimated_wallet_amount — этот номинал, умноженный на курс: оценка, а не гарантия итогового зачисления. amount остаётся запрошенной суммой.
custom_amount
Актуальные min, max и step для каждой валюты указаны в wallet_currencies. custom_amount описывает поля amount/currency и ограничения в эквиваленте USD. Не задавайте пересчитанные границы вручную.
discount_percent / discount_amount_usdt
discount_percent и discount_amount_usdt показывают скидку Steam 3.5% от base_price_usdt. Итоговую price_usdt вычисляет сервер; не округляйте её до центов и не пересчитывайте в клиенте.
expected_price_usdt
Передайте принятую price_usdt в expected_price_usdt. Если сумма списания изменилась, до списания вернётся STEAM_QUOTE_CHANGED. Получите новый расчёт и явное подтверждение перед новой попыткой.
2. Заказ пополнения по логину Steam — scope order
# Set these from the accepted quote: STEAM_AMOUNT, STEAM_CURRENCY,
# STEAM_PRICE_USDT (= price_usdt). Persist STEAM_ORDER_KEY for this order.
# STEAM_LOGIN is the recipient login; STEAM_GAME_KEY is the same catalog ID.
python3 - <<'PY' > steam-order.json
import json, os
print(json.dumps({
    "game_key": os.environ["STEAM_GAME_KEY"],
    "amount": os.environ["STEAM_AMOUNT"],
    "currency": os.environ["STEAM_CURRENCY"],
    "expected_price_usdt": os.environ["STEAM_PRICE_USDT"],
    "player_id": os.environ["STEAM_LOGIN"],
}))
PY
curl -X POST "https://api.ludovex.org/api/v2/order" \
  -H "X-API-Key: $LUDOVEX_API_KEY" \
  -H "Idempotency-Key: $STEAM_ORDER_KEY" \
  -H "Content-Type: application/json" --data-binary @steam-order.json

В новом контракте всегда передавайте amount и currency. Один variation_id их не заменяет. player_id — логин Steam.

Старое amount_usd остаётся явным вариантом для USD; custom_amount_uzs также поддерживается. Используйте только один вариант суммы в запросе.

После тайм-аута повторяйте тот же заказ с тем же ключом идемпотентности. Потеря ответа не повод создавать новый ключ. Новый ключ нужен для другого подтверждённого заказа или явного отказа цены до списания. Следите за status: 201 может означать обработку.

Неизвестная валюта, сумма JSON-числом, более 2 знаков после точки, смешанные варианты суммы или выход за границы отклоняются. Если курс или цена недоступны, не используйте приблизительный расчёт.

Подписанные webhook-события

Настройте публичный адрес приёмника и события в Кабинет → Настройки → Исходящий webhook. Доставка выполняется как минимум один раз, поэтому обработчик должен быть идемпотентным.

Тело имеет вид { event, data }. Одно событие может поступить несколько раз.

  • order.completedЗаказ выполнен; проверьте доставленные данные.
  • order.failedЗаказ завершился ошибкой.
  • order.refundedЗафиксирован возврат средств по заказу.
  • payment.succeededПодтверждено пополнение баланса.

Проверяйте X-Ludovex-Signature как sha256=HMAC_SHA256(secret, timestamp + "." + rawBody), используя X-Ludovex-Timestamp и точные исходные байты запроса. Сравнивайте за постоянное время и отклоняйте отсутствующие, неверные или устаревшие метки времени; в примерах допускается разница часов до пяти минут.

Используйте X-Ludovex-Delivery-Id для постоянного учёта повторных доставок. Повтор сохраняет ID и тело, но получает новую метку времени и подпись. Подписанное тестовое событие не содержит delivery-ID: проверьте подпись, подтвердите приём и не выполняйте бизнес-операции.

Примеры подтверждают бизнес-событие только после записи в закрытую SQLite-таблицу входящих событий с уникальным delivery ID. Отдельный обработчик должен читать необработанные строки и атомарно фиксировать бизнес-операцию и завершение. Ответ 2xx до постоянного сохранения может привести к потере события.

Обеспечьте идемпотентность бизнес-операций также по подписанному идентификатору заказа/платежа и событию. Заголовок delivery-ID не входит в HMAC. Не зачисляйте средства только на основании верной подписи. Синхронизируйте часы, защищайте таблицу и секрет, не записывайте доставленное содержимое в журналы.

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

Проверяйте статус доставки, попытки и HTTP-ответы в истории webhook в кабинете. Разовые тестовые события в этой истории не сохраняются.

Открыть историю доставки webhook

Пример Node: Node 24+, Express и WEBHOOK_SECRET. Пример Python: Flask и WEBHOOK_SECRET. WEBHOOK_INBOX_DIR должен быть закрытым постоянным каталогом. Эти примеры для одного сервера слушают loopback за вашим HTTPS-прокси; для нескольких серверов используйте общую транзакционную таблицу входящих событий.

Проверить, сохранить, затем подтвердить
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import express from 'express';

const secret = process.env.WEBHOOK_SECRET;
if (!secret) throw new Error('WEBHOOK_SECRET is required');
const directory = process.env.WEBHOOK_INBOX_DIR ?? './private-webhooks';
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
const file = path.join(directory, 'inbox.sqlite');
const db = new DatabaseSync(file);
fs.chmodSync(file, 0o600);
db.exec('PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL;');
db.exec('CREATE TABLE IF NOT EXISTS inbox (' +
  'delivery_id TEXT PRIMARY KEY, payload BLOB NOT NULL, payload_hash TEXT NOT NULL,' +
  'received_at INTEGER NOT NULL, processed_at INTEGER)');
const insert = db.prepare('INSERT OR IGNORE INTO inbox (delivery_id,payload,payload_hash,received_at) VALUES (?,?,?,?)');
const existing = db.prepare('SELECT payload_hash FROM inbox WHERE delivery_id=?');
const app = express();

app.post('/webhooks/ludovex', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => {
  const raw = req.body;
  const ts = req.get('X-Ludovex-Timestamp');
  const sig = req.get('X-Ludovex-Signature');
  if (!Buffer.isBuffer(raw) || !/^\d{10}$/.test(ts ?? '') ||
      Math.abs(Date.now() / 1000 - Number(ts)) > 300 || !/^sha256=[a-f0-9]{64}$/.test(sig ?? '')) {
    return res.sendStatus(401);
  }
  const expected = crypto.createHmac('sha256', secret).update(ts + '.').update(raw).digest();
  const received = Buffer.from(sig.slice(7), 'hex');
  if (!crypto.timingSafeEqual(expected, received)) return res.sendStatus(401);
  let event;
  try { event = JSON.parse(raw.toString('utf8')); } catch { return res.sendStatus(400); }
  if (!event || typeof event.event !== 'string' || !event.data || typeof event.data !== 'object') return res.sendStatus(400);
  if (event.event === 'test') return res.sendStatus(204);
  const deliveryId = req.get('X-Ludovex-Delivery-Id');
  if (!/^[1-9]\d{0,19}$/.test(deliveryId ?? '')) return res.sendStatus(400);
  const hash = crypto.createHash('sha256').update(raw).digest('hex');
  try {
    insert.run(deliveryId, raw, hash, Math.floor(Date.now() / 1000));
    if (existing.get(deliveryId)?.payload_hash !== hash) return res.sendStatus(409);
    return res.sendStatus(204);
  } catch {
    return res.sendStatus(503);
  }
});
app.listen(3000, '127.0.0.1');
Документация API реселлера: каталог и заказы | Ludovex