OYUN QURDU Corporate
Справочник API · v1

Oyun Qurdu Corporate API

REST API для покупки пополнений в играх и цифровых подарочных карт по оптовым ценам и их выдачи в вашем собственном продукте. Заказы оплачиваются с предоплаченного баланса вашего аккаунта.

Базовый URL
https://corporative.oyunqurdu.az/api/v1
  • Все запросы и ответы — JSON, UTF-8. Сообщения в ответах API — на английском, ваш код может на них опираться.
  • Только HTTPS. Обычный HTTP перенаправляется и не должен использоваться для API.
  • Версия — часть пути (/v1). Мы добавляем поля без смены версии и никогда не удаляем и не переименовываем их в рамках v1.
  • Два вида товаров: пополнение (зачисляется прямо на игровой аккаунт — вы передаёте ID игрока) и код (подарочные карты — код приходит в ответе).

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

1. Войдите и создайте API-ключ на странице API-ключей. 2. Убедитесь, что баланс больше нуля. 3. Найдите товар, проверьте живую цену и закажите:

# 1. find the product code
curl "https://corporative.oyunqurdu.az/api/v1/products?category=pubg-mobile" \
  -H "Authorization: Bearer $OQ_API_KEY"

# 2. live price right now
curl "https://corporative.oyunqurdu.az/api/v1/products/PUBGM-660-UC/quote" \
  -H "Authorization: Bearer $OQ_API_KEY"

# 3. order it (max_unit_price locks the quoted price)
curl -X POST https://corporative.oyunqurdu.az/api/v1/orders \
  -H "Authorization: Bearer $OQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"product": "PUBGM-660-UC", "fields": {"player_id": "5123456789"}, "reference": "INV-10045"}'

# 4. read the result (or wait for the webhook)
curl https://corporative.oyunqurdu.az/api/v1/orders/reference/INV-10045 \
  -H "Authorization: Bearer $OQ_API_KEY"

Аутентификация

Каждому запросу (кроме GET /ping и GET /openapi.json) нужен API-ключ. Передайте его как bearer-токен:

Authorization: Bearer oqc_Ab3dE5fG7h_••••••••••••••••••••••••••••••••••••••••

Заголовок X-API-Key: oqc_… тоже принимается.

  • Ключи начинаются с oqc_. Их может быть несколько (например, по одному на сервер), и каждый можно отозвать отдельно.
  • Ключ показывается один раз — при создании. Мы храним только его отпечаток; утерянный ключ не восстановить — создайте новый.
  • Каждый ключ можно ограничить: разрешённые IP, разрешённые сайты, дневной лимит расходов или только чтение (без заказов).
  • Храните ключи на сервере. Никогда не помещайте их в мобильное приложение, браузерный код или публичный репозиторий.

Использование с сайта

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

  • Лучше всего: вызывайте API со своего сервера (или небольшого прокси) и храните ключ там. Ваша страница общается с вашим сервером, сервер — с нами.
  • У Google Sites нет своего сервера, а встроенный код работает на общем домене Google (*.googleusercontent.com) — привязка к домену не отличит ваш сайт от любого другого. Используйте веб-приложение Google Apps Script как прокси (пример ниже): ключ остаётся в свойствах скрипта на серверах Google и никогда не попадает в браузер.
  • Если без браузера никак — добавьте домен сайта к ключу (Разрешённые сайты), задайте дневной лимит расходов и подумайте об отдельном ключе только для чтения для цен. Ключ с сайтами отвечает только на запросы с этих сайтов; ключ без сайтов отклоняет браузерные запросы полностью.
Google Apps Script · Code.gs
// Extensions → Apps Script. Project Settings → Script properties → OQ_API_KEY = oqc_…
// Deploy → New deployment → Web app. Embed its URL in Google Sites.
const API = "https://corporative.oyunqurdu.az/api/v1";
const KEY = PropertiesService.getScriptProperties().getProperty("OQ_API_KEY");

function call_(method, path, body) {
  const res = UrlFetchApp.fetch(API + path, {
    method: method,
    contentType: "application/json",
    headers: { Authorization: "Bearer " + KEY },
    payload: body ? JSON.stringify(body) : undefined,
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText());
}

function getPrice(code) {                       // google.script.run.getPrice("PUBGM-660-UC")
  return call_("get", "/products/" + encodeURIComponent(code) + "/quote");
}

function placeOrder(code, playerId, reference) { // add your own checks here
  return call_("post", "/orders", {
    product: code, fields: { player_id: playerId }, reference: reference,
  });
}

function doGet() { return HtmlService.createHtmlOutputFromFile("Index"); }
Любой, кто может открыть веб-приложение, может его вызвать. Добавьте свои проверки в скрипт (например, только после оплаты клиентом) и держите на ключе дневной лимит.

Запросы и ответы

  • Деньги — строка с двумя знаками после точки ("9.17"), никогда не float. Валюта — валюта аккаунта (USD).
  • Время — ISO 8601 в UTC (2026-10-24T09:12:44Z).
  • У ID есть префиксы: ord_ заказы, txn_ транзакции, evt_ события вебхуков, acc_ аккаунты.
  • Списки разбиты на страницы через page и per_page (до 100) и возвращают data и pagination.
  • В каждом ответе есть X-Request-Id. Указывайте его, когда пишете нам о запросе.
  • Слэш в конце необязателен: /orders и /orders/ — одно и то же.

Ошибки

Ошибки используют стандартные HTTP-коды и всегда имеют одну форму. Опирайтесь на error.code — сообщение может меняться.

{
  "error": {
    "code": "insufficient_balance",
    "message": "Your balance is too low for this order.",
    "retryable": false,
    "details": {
      "balance": "4.20",
      "required": "9.17",
      "shortfall": "4.97"
    },
    "doc_url": "https://corporative.oyunqurdu.az/docs/#error-insufficient_balance"
  },
  "request_id": "req_3f9a1c0b7d2e4a6b8c1d"
}

Каждая ошибка также сообщает, поможет ли повтор (retryable), и ссылается на свою строку ниже (doc_url).

codeHTTPПовтор?ЗначениеЧто делать
unauthorized401 нет API-ключ не передан.Передайте `Authorization: Bearer <key>` (или `X-API-Key`).
malformed_api_key401 нет Это не API-ключ (ключи выглядят как `oqc_…_…`).Скопируйте ключ целиком; проверьте пробелы и кавычки.
invalid_api_key401 нет Такого API-ключа нет.Проверьте ключ или создайте новый в панели.
api_key_revoked401 нет Ключ отозван.Создайте новый ключ в панели.
account_disabled403 нет Аккаунт отключён.Свяжитесь с нами.
ip_not_allowed403 нет Запрос пришёл с IP, которого нет в списке разрешённых для ключа.Добавьте IP к ключу в панели. `details.ip` — адрес, который мы видим.
origin_not_allowed403 нет Запрос пришёл с сайта, которого нет в списке сайтов ключа.Добавьте домен к ключу в панели. `details.origin` — то, что мы видим.
key_read_only403 нет Этот ключ может только читать, но не может создавать заказы.Используйте ключ с правом заказа или включите его в панели.
invalid_json400 нет Тело запроса — не корректный JSON-объект.Отправьте JSON-объект с `Content-Type: application/json`.
unsupported_media_type415 нет Тело запроса — не JSON.Укажите `Content-Type: application/json`.
payload_too_large413 нет Тело запроса больше 32 КБ.Отправьте тело поменьше.
validation_error422 нет Один или несколько параметров отсутствуют или неверны.`details` перечисляет каждый параметр с `code` и `message`.
invalid_parameter422 нет Неверный параметр запроса.`details` называет параметр и допустимые значения.
invalid_fields422 нет Поля товара (ID игрока, сервер …) отсутствуют или неверны.В `details.fields.<key>` есть `code`: required, unknown_field, too_long, invalid_option, invalid_type, invalid_characters.
not_found404 нет Такого эндпоинта нет.Проверьте путь и префикс `/v1`.
method_not_allowed405 нет Этот HTTP-метод здесь не поддерживается.Смотрите заголовок `Allow`.
product_not_found404 нет Товара с таким кодом нет.Получите список товаров через `GET /products`.
product_discontinued410 нет Товар больше не продаётся.Уберите его из каталога; выберите другой товар.
product_unavailable409 да Товар временно недоступен.Повторите позже или предложите другой товар.
out_of_stock409 да Недостаточно товара на складе для этого количества.`details.available` — остаток. Уменьшите количество или повторите позже.
quantity_out_of_range422 нет Количество вне допустимых пределов товара.В `details` есть `min` и `max`.
not_supported422 нет Эта операция недоступна для этого товара.
insufficient_balance402 нет Ваш баланс меньше суммы заказа.Пополните баланс. В `details` есть `balance`, `required` и `shortfall`.
order_limit_exceeded422 нет Сумма заказа выше лимита на один заказ для вашего аккаунта.Разделите заказ или попросите нас поднять лимит. В `details` есть `total` и `limit`.
key_daily_limit_exceeded422 нет Этот ключ достиг дневного лимита расходов.Дождитесь 00:00 UTC или увеличьте лимит ключа в панели.
price_changed409 да Текущая цена выше вашего `max_unit_price`.Перечитайте цену (`details.unit_price`) и повторите.
duplicate_reference409 нет Этот `reference` уже использован с другими параметрами.Используйте новый reference. `details.order_id` — существующий заказ.
order_not_found404 нет Заказа с таким ID или reference нет.Проверьте ID.
rate_limited429 да Слишком много запросов за эту минуту.Подождите `Retry-After` секунд.
maintenance503 да Приём заказов приостановлен на короткое обслуживание.Повторите через `Retry-After` секунд. С баланса ничего не списано. Просмотр работает.
service_unavailable503 да Часть сервиса временно недоступна.Повторите позже. Ничего не списано.
internal_error500 да Непредвиденная ошибка на нашей стороне.Повторите. Если повторится — пришлите нам `request_id`.

Ошибки полей

validation_error (параметры тела) и invalid_fields (поля товара) объясняют каждую проблему отдельно:

{
  "error": {
    "code": "invalid_fields",
    "message": "Some product fields are missing or invalid.",
    "retryable": false,
    "details": {"fields": {
      "server_id": {"code": "required", "message": "Server ID is required."},
      "zone":      {"code": "unknown_field", "message": "This product does not take `zone`. Expected: player_id, server_id."}
    }},
    "doc_url": "https://corporative.oyunqurdu.az/docs/#error-invalid_fields"
  },
  "request_id": "req_3f9a1c0b7d2e4a6b8c1d"
}
Списали ли деньги? Если POST /orders вернул любую ошибку, баланс не тронут. Деньги списываются только при создании заказа (201).
Можно ли повторять? Ошибки с "retryable": true могут пройти позже — повторяйте с паузами (с тем же reference). Остальные требуют сначала изменений с вашей стороны.

Обслуживание

Иногда приём заказов приостанавливается на короткое обслуживание. В это время:

  • POST /orders возвращает 503 maintenance с заголовком Retry-After (секунды). Ничего не списывается.
  • Всё остальное — товары, цены, баланс, заказы, транзакции — продолжает работать.
  • GET /ping и GET /account показывают это в service:
{"status": "ok", "version": "1.0.0",
 "service": {"status": "maintenance", "message": "Ordering is paused for a short maintenance…", "retry_after": 300}}

Обычно service.status — operational. Покажите клиентам короткое «попробуйте через несколько минут» и повторите после Retry-After.

Лимиты запросов

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

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 41        # seconds until the window resets

При превышении лимита вы получите 429 rate_limited с заголовком Retry-After. Нужно больше? Напишите нам.

Идемпотентность

Сеть иногда падает. Если POST /orders не дождался ответа, вы не узнаете, создан ли заказ, — поэтому всегда передавайте свой reference (номер счёта или транзакции, уникальный для каждого заказа):

  • Тот же reference, тот же товар и количество → вы получите существующий заказ с 200 и Idempotent-Replayed: true. Дважды ничего не списывается.
  • Тот же reference, другие параметры → 409 duplicate_reference.
  • Найти заказ по reference можно в любой момент: GET /orders/reference/{reference}.

Аккаунт и баланс

GET/account

Ваша компания, логин, баланс, лимит запросов, настройки вебхуков и статус сервиса.

GET/balance
{"balance": "1250.40", "currency": "USD"}

Баланс пополняет наша команда после вашего перевода. Каждое движение видно в транзакциях.

Категории

GET/categories

Игры и сервисы с доступными регионами для каждого.

{"data": [
  {"code": "free-fire", "name": "Free Fire", "regions": ["CIS", "EU", "ID"], "product_count": 37},
  {"code": "steam", "name": "Steam Wallet", "regions": ["CIS", "GLOBAL"], "product_count": 29}
]}

Товары

GET/products

Все товары с вашими ценами. Просмотр кодов не требует обращения к рынку — ответ мгновенный. Фильтры:

categoryКод категории, напр. free-fire
regionGLOBAL, EU, TR, US, CIS, RU, ID
typetopup или code
qПоиск по названию и коду
in_stocktrue — только товары, доступные для заказа сейчас
livetrue — переоценить категорию по рынку сейчас (нужен category)
page, per_pageПагинация (per_page ≤ 100)
GET/products/{code}
{
  "code": "PUBGM-660-UC",
  "name": "PUBG Mobile — 660 UC",
  "category": "pubg-mobile",
  "category_name": "PUBG Mobile",
  "region": "GLOBAL",
  "type": "topup",
  "price": "9.17",
  "currency": "USD",
  "in_stock": true,
  "stock": null,
  "min_quantity": 1,
  "max_quantity": 10,
  "fields": [
    {
      "key": "player_id",
      "label": "Player ID",
      "type": "text",
      "required": true
    }
  ],
  "description": "Enter your Player ID before placing the order."
}
codeПостоянный код товара. Никогда не меняется — храните его в своей системе.
typetopup: зачисляется на игровой аккаунт из fields. code: вы получаете коды подарочных карт.
priceВаша цена за единицу, обновляется с рынка минимум раз в 15 минут (price_updated_at). Для точной цены прямо сейчас используйте эндпоинт quote.
stockДоступное количество для кодов. null — не ограничено складом (пополнения).
min_quantity / max_quantityДопустимое количество в одном заказе.
fieldsЧто нужно передать в fields при заказе — напр. player_id, server_id. Для кодов пусто.

Живые цены

GET/products/{code}/quote?quantity=2

Запрашивает цену на рынке прямо сейчас, добавляет вашу наценку и возвращает цену за единицу и итог. Используйте прямо перед показом цены клиенту.

{
  "product": "PUBGM-660-UC",
  "name": "PUBG Mobile — 660 UC",
  "quantity": 2,
  "unit_price": "9.17",
  "total": "18.34",
  "currency": "USD",
  "available": true,
  "in_stock": true,
  "stock": null,
  "live": true,
  "quoted_at": "2026-10-24T09:12:40Z",
  "valid_until": "2026-10-24T09:13:40Z",
  "hint": "Send max_unit_price with the order to lock this price."
}
  • live: true — цена получена сейчас. false — рынок не ответил, это последняя известная цена.
  • Цены меняются. Передайте с заказом max_unit_price (полученный unit_price): если цена за это время выросла, заказ отклоняется с 409 price_changed, и ничего не списывается.
  • Для всей категории сразу: GET /products?category=pubg-mobile&live=true.

Проверка ID игрока

POST/products/{code}/validate

Для товаров пополнения: проверьте игровой аккаунт до списания денег с клиента. Не все игры это поддерживают — тогда вы получите 422 not_supported.

POST /products/MLBB-GLOBAL-86-DIAMONDS/validate
{"fields": {"player_id": "123456789", "server_id": "2001"}}

→ {"valid": true, "player_name": "ShadowFox"}

Создать заказ

POST/orders
ПолеОбязательноОписание
productдаКод товара.
quantityнетПо умолчанию 1. Должно быть в пределах min/max товара.
fieldsдля пополненийОбъект со всеми ключами из fields товара. Неизвестные ключи отклоняются.
referenceрекомендуетсяВаш уникальный ID (1–64 символа: буквы, цифры, _ . : -). Делает повтор запроса безопасным.
max_unit_priceнетОтклонить заказ с 409 price_changed, если цена за единицу теперь выше.
callback_urlнетhttps-адрес, получающий вебхуки этого заказа, в дополнение к вебхуку аккаунта.

Сумма сразу списывается с баланса, а заказ начинается со статуса processing:

201 Created
{
  "id": "ord_7kq2m9xw4hz8n3pd5t",
  "reference": "INV-10045",
  "status": "processing",
  "status_message": "The order is being fulfilled.",
  "product": {
    "code": "STEAM-GLOBAL-20-USD",
    "name": "Steam Wallet (Global) — 20 USD"
  },
  "quantity": 2,
  "unit_price": "21.62",
  "total": "43.24",
  "currency": "USD",
  "fields": {},
  "delivery": null,
  "failure": null,
  "created_at": "2026-10-24T09:12:44Z",
  "updated_at": "2026-10-24T09:12:47Z",
  "completed_at": null
}

Перед списанием мы проверяем живую цену, наличие и доступность. Если что-то не сходится, вы получите ошибку, и ничего не будет списано.

Жизненный цикл заказа

statusФинальныйЗначение
processingнетВыполняется. Обычно секунды; некоторые пополнения занимают несколько минут.
completedдаГотово. Коды в delivery.codes; для пополнений аккаунт пополнен.
failedдаНе удалось выполнить. Сумма возвращена на баланс. Смотрите failure.code.
on_holdнетНа ручной проверке у нашей команды (редко). Перейдёт в completed или refunded — ничего делать не нужно.
refundedдаВозвращено на баланс нашей командой.

Выполненный заказ:

{
  "id": "ord_7kq2m9xw4hz8n3pd5t",
  "reference": "INV-10045",
  "status": "completed",
  "status_message": "The order is complete.",
  "product": {
    "code": "STEAM-GLOBAL-20-USD",
    "name": "Steam Wallet (Global) — 20 USD"
  },
  "quantity": 2,
  "unit_price": "21.62",
  "total": "43.24",
  "currency": "USD",
  "fields": {},
  "delivery": {
    "type": "code",
    "codes": [
      "XXXXX-XXXXX-XXXXX",
      "YYYYY-YYYYY-YYYYY"
    ],
    "message": null
  },
  "failure": null,
  "created_at": "2026-10-24T09:12:44Z",
  "updated_at": "2026-10-24T09:12:47Z",
  "completed_at": "2026-10-24T09:12:47Z"
}

Коды неудачи

Когда заказ завершается со статусом failed, failure.code объясняет причину. Сумма всегда возвращается на баланс.

failure.codeЗначениеЧто делать
invalid_account_idID игрового аккаунта (или сервер) отклонён.Исправьте поля и создайте новый заказ.
out_of_stockТовар закончился до выполнения заказа.Повторите позже.
product_unavailableВо время выполнения товар стал недоступен.Предложите другой товар.
maintenanceПриём заказов был приостановлен на короткое обслуживание.Повторите через несколько минут.
fulfillment_failedЗаказ не удалось выполнить.Повторите один раз; если повторится — напишите нам.
refundedЗаказ возвращён нашей командой.

Получение заказов

GET/orders/{id}
GET/orders/reference/{reference}
GET/orders

Фильтры списка: status, product, reference, from, to (даты ISO), page, per_page. Сначала новые.

Опрос: проверяйте каждые 2–5 секунд, пока processing; остановитесь после финального статуса. Вебхуки избавляют от опроса.

Транзакции

GET/transactions

Каждое движение баланса, сначала новые, с балансом после него. Типы: deposit, purchase, refund, adjustment, withdrawal. Фильтры: type, from, to.

{"data": [
  {"id": "txn_k2m8q4w7x3z9n5p6r1", "type": "purchase", "amount": "-9.17",
   "balance_after": "1241.23", "currency": "USD", "order_id": "ord_7kq2m9xw4hz8n3pd5t",
   "description": "PUBGM-660-UC × 1", "created_at": "2026-10-24T09:12:44Z"}
], "pagination": {"page": 1, "per_page": 50, "total": 1, "total_pages": 1}}

Вебхуки

Укажите URL вебхука на странице «Вебхуки» в панели (или для отдельного заказа через callback_url). При каждой смене статуса заказа мы отправляем POST с событием order.updated:

POST https://your-server.example/oq/webhook
Content-Type: application/json
X-OQ-Event: order.updated
X-OQ-Delivery: evt_9w3k7m2q8x4z6n5p1r
X-OQ-Signature: t=1792833167,v1=5f1c…e9a2

{
  "id": "evt_9w3k7m2q8x4z6n5p1r",
  "type": "order.updated",
  "created_at": "2026-10-24T09:12:47Z",
  "data": {
    "order": {
      "id": "ord_7kq2m9xw4hz8n3pd5t",
      "reference": "INV-10045",
      "status": "completed",
      "status_message": "The order is complete.",
      "product": {
        "code": "STEAM-GLOBAL-20-USD",
        "name": "Steam Wallet (Global) — 20 USD"
      },
      "quantity": 2,
      "unit_price": "21.62",
      "total": "43.24",
      "currency": "USD",
      "fields": {},
      "delivery": {
        "type": "code",
        "codes": [
          "XXXXX-XXXXX-XXXXX",
          "YYYYY-YYYYY-YYYYY"
        ],
        "message": null
      },
      "failure": null,
      "created_at": "2026-10-24T09:12:44Z",
      "updated_at": "2026-10-24T09:12:47Z",
      "completed_at": "2026-10-24T09:12:47Z"
    }
  }
}
  • Ответьте любым 2xx в течение 10 секунд. Иначе мы повторим через 1 мин, 5 мин, 30 мин, 2 ч и 6 ч.
  • События могут приходить повторно и не по порядку. Используйте data.order.status и updated_at и игнорируйте события по заказам, уже финальным в вашей системе.
  • ID X-OQ-Delivery уникален для каждого события — используйте его, чтобы отбрасывать дубли.

Проверка подписи

Каждый вебхук подписан вашим секретом (whsec_…, на странице «Вебхуки»). X-OQ-Signature имеет вид t=<unix time>,v1=<hex>, где hex = HMAC-SHA256(secret, "<t>.<raw request body>"). Отклоняйте запрос, если подпись не совпадает или t старше 5 минут.

import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
Используйте сырое тело. Проверяйте байты ровно в том виде, как получили, — разбор и повторная сериализация JSON меняют их, и подпись не совпадёт.

Рекомендации

  • Всегда передавайте reference. При таймауте повторите с тем же reference или найдите заказ по нему.
  • Перед оплатой получите цену через /quote и передайте её как max_unit_price.
  • Кэшируйте GET /products на несколько минут, а не вызывайте перед каждым заказом.
  • Где возможно, проверяйте ID игроков через /validate и копируйте ключи fields точно.
  • Считайте 503 временной ошибкой — повторите позже; ничего не списано.
  • Для боевых ключей используйте отдельный ключ на сервер, список IP и дневной лимит.
  • Держите таймаут на POST /orders не меньше 30 секунд.

OpenAPI

Полная машиночитаемая спецификация (OpenAPI 3.1) для Postman, Insomnia или генераторов кода. Интерактивная версия — на странице справочника API.

Спецификация Открыть
https://corporative.oyunqurdu.az/api/v1/openapi.json

История изменений

v1.0.0Первый выпуск: товары, живые цены, заказы, баланс, транзакции, вебхуки, проверка ID игрока, структурированные ошибки, статус обслуживания, ограничения ключей (IP, сайты, дневной лимит, только чтение).