Oyun Qurdu Corporate API
REST API для покупки пополнений в играх и цифровых подарочных карт по оптовым ценам и их выдачи в вашем собственном продукте. Заказы оплачиваются с предоплаченного баланса вашего аккаунта.
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"
import os, time, requests
API = "https://corporative.oyunqurdu.az/api/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['OQ_API_KEY']}"
order = s.post(f"{API}/orders", json={
"product": "PUBGM-660-UC",
"fields": {"player_id": "5123456789"},
"reference": "INV-10045", # your own unique ID
}, timeout=30).json()
while order["status"] == "processing":
time.sleep(3)
order = s.get(f"{API}/orders/{order['id']}", timeout=30).json()
print(order["status"], order["delivery"])
<?php
$api = "https://corporative.oyunqurdu.az/api/v1";
$ch = curl_init("$api/orders");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("OQ_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"product" => "PUBGM-660-UC",
"fields" => ["player_id" => "5123456789"],
"reference" => "INV-10045",
]),
]);
$order = json_decode(curl_exec($ch), true);
echo $order["id"], " ", $order["status"];
const API = "https://corporative.oyunqurdu.az/api/v1";
const headers = {
"Authorization": `Bearer ${process.env.OQ_API_KEY}`,
"Content-Type": "application/json",
};
const res = await fetch(`${API}/orders`, {
method: "POST",
headers,
body: JSON.stringify({
product: "PUBGM-660-UC",
fields: { player_id: "5123456789" },
reference: "INV-10045",
}),
});
const order = await res.json();
console.log(order.id, order.status);
Аутентификация
Каждому запросу (кроме 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 и никогда не попадает в браузер. - Если без браузера никак — добавьте домен сайта к ключу (Разрешённые сайты), задайте дневной лимит расходов и подумайте об отдельном ключе только для чтения для цен. Ключ с сайтами отвечает только на запросы с этих сайтов; ключ без сайтов отклоняет браузерные запросы полностью.
// 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).
| code | HTTP | Повтор? | Значение | Что делать |
|---|---|---|---|---|
| unauthorized | 401 | нет | API-ключ не передан. | Передайте `Authorization: Bearer <key>` (или `X-API-Key`). |
| malformed_api_key | 401 | нет | Это не API-ключ (ключи выглядят как `oqc_…_…`). | Скопируйте ключ целиком; проверьте пробелы и кавычки. |
| invalid_api_key | 401 | нет | Такого API-ключа нет. | Проверьте ключ или создайте новый в панели. |
| api_key_revoked | 401 | нет | Ключ отозван. | Создайте новый ключ в панели. |
| account_disabled | 403 | нет | Аккаунт отключён. | Свяжитесь с нами. |
| ip_not_allowed | 403 | нет | Запрос пришёл с IP, которого нет в списке разрешённых для ключа. | Добавьте IP к ключу в панели. `details.ip` — адрес, который мы видим. |
| origin_not_allowed | 403 | нет | Запрос пришёл с сайта, которого нет в списке сайтов ключа. | Добавьте домен к ключу в панели. `details.origin` — то, что мы видим. |
| key_read_only | 403 | нет | Этот ключ может только читать, но не может создавать заказы. | Используйте ключ с правом заказа или включите его в панели. |
| invalid_json | 400 | нет | Тело запроса — не корректный JSON-объект. | Отправьте JSON-объект с `Content-Type: application/json`. |
| unsupported_media_type | 415 | нет | Тело запроса — не JSON. | Укажите `Content-Type: application/json`. |
| payload_too_large | 413 | нет | Тело запроса больше 32 КБ. | Отправьте тело поменьше. |
| validation_error | 422 | нет | Один или несколько параметров отсутствуют или неверны. | `details` перечисляет каждый параметр с `code` и `message`. |
| invalid_parameter | 422 | нет | Неверный параметр запроса. | `details` называет параметр и допустимые значения. |
| invalid_fields | 422 | нет | Поля товара (ID игрока, сервер …) отсутствуют или неверны. | В `details.fields.<key>` есть `code`: required, unknown_field, too_long, invalid_option, invalid_type, invalid_characters. |
| not_found | 404 | нет | Такого эндпоинта нет. | Проверьте путь и префикс `/v1`. |
| method_not_allowed | 405 | нет | Этот HTTP-метод здесь не поддерживается. | Смотрите заголовок `Allow`. |
| product_not_found | 404 | нет | Товара с таким кодом нет. | Получите список товаров через `GET /products`. |
| product_discontinued | 410 | нет | Товар больше не продаётся. | Уберите его из каталога; выберите другой товар. |
| product_unavailable | 409 | да | Товар временно недоступен. | Повторите позже или предложите другой товар. |
| out_of_stock | 409 | да | Недостаточно товара на складе для этого количества. | `details.available` — остаток. Уменьшите количество или повторите позже. |
| quantity_out_of_range | 422 | нет | Количество вне допустимых пределов товара. | В `details` есть `min` и `max`. |
| not_supported | 422 | нет | Эта операция недоступна для этого товара. | |
| insufficient_balance | 402 | нет | Ваш баланс меньше суммы заказа. | Пополните баланс. В `details` есть `balance`, `required` и `shortfall`. |
| order_limit_exceeded | 422 | нет | Сумма заказа выше лимита на один заказ для вашего аккаунта. | Разделите заказ или попросите нас поднять лимит. В `details` есть `total` и `limit`. |
| key_daily_limit_exceeded | 422 | нет | Этот ключ достиг дневного лимита расходов. | Дождитесь 00:00 UTC или увеличьте лимит ключа в панели. |
| price_changed | 409 | да | Текущая цена выше вашего `max_unit_price`. | Перечитайте цену (`details.unit_price`) и повторите. |
| duplicate_reference | 409 | нет | Этот `reference` уже использован с другими параметрами. | Используйте новый reference. `details.order_id` — существующий заказ. |
| order_not_found | 404 | нет | Заказа с таким ID или reference нет. | Проверьте ID. |
| rate_limited | 429 | да | Слишком много запросов за эту минуту. | Подождите `Retry-After` секунд. |
| maintenance | 503 | да | Приём заказов приостановлен на короткое обслуживание. | Повторите через `Retry-After` секунд. С баланса ничего не списано. Просмотр работает. |
| service_unavailable | 503 | да | Часть сервиса временно недоступна. | Повторите позже. Ничего не списано. |
| internal_error | 500 | да | Непредвиденная ошибка на нашей стороне. | Повторите. Если повторится — пришлите нам `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}.
Аккаунт и баланс
Ваша компания, логин, баланс, лимит запросов, настройки вебхуков и статус сервиса.
{"balance": "1250.40", "currency": "USD"}Баланс пополняет наша команда после вашего перевода. Каждое движение видно в транзакциях.
Категории
Игры и сервисы с доступными регионами для каждого.
{"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}
]}Товары
Все товары с вашими ценами. Просмотр кодов не требует обращения к рынку — ответ мгновенный. Фильтры:
| category | Код категории, напр. free-fire |
| region | GLOBAL, EU, TR, US, CIS, RU, ID |
| type | topup или code |
| q | Поиск по названию и коду |
| in_stock | true — только товары, доступные для заказа сейчас |
| live | true — переоценить категорию по рынку сейчас (нужен category) |
| page, per_page | Пагинация (per_page ≤ 100) |
{
"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 | Постоянный код товара. Никогда не меняется — храните его в своей системе. |
| type | topup: зачисляется на игровой аккаунт из fields. code: вы получаете коды подарочных карт. |
| price | Ваша цена за единицу, обновляется с рынка минимум раз в 15 минут (price_updated_at). Для точной цены прямо сейчас используйте эндпоинт quote. |
| stock | Доступное количество для кодов. null — не ограничено складом (пополнения). |
| min_quantity / max_quantity | Допустимое количество в одном заказе. |
| fields | Что нужно передать в fields при заказе — напр. player_id, server_id. Для кодов пусто. |
Живые цены
Запрашивает цену на рынке прямо сейчас, добавляет вашу наценку и возвращает цену за единицу и итог. Используйте прямо перед показом цены клиенту.
{
"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 игрока
Для товаров пополнения: проверьте игровой аккаунт до списания денег с клиента. Не все игры это поддерживают — тогда вы получите 422 not_supported.
POST /products/MLBB-GLOBAL-86-DIAMONDS/validate
{"fields": {"player_id": "123456789", "server_id": "2001"}}
→ {"valid": true, "player_name": "ShadowFox"}Создать заказ
| Поле | Обязательно | Описание |
|---|---|---|
| product | да | Код товара. |
| quantity | нет | По умолчанию 1. Должно быть в пределах min/max товара. |
| fields | для пополнений | Объект со всеми ключами из fields товара. Неизвестные ключи отклоняются. |
| reference | рекомендуется | Ваш уникальный ID (1–64 символа: буквы, цифры, _ . : -). Делает повтор запроса безопасным. |
| max_unit_price | нет | Отклонить заказ с 409 price_changed, если цена за единицу теперь выше. |
| callback_url | нет | https-адрес, получающий вебхуки этого заказа, в дополнение к вебхуку аккаунта. |
Сумма сразу списывается с баланса, а заказ начинается со статуса processing:
{
"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_id | ID игрового аккаунта (или сервер) отклонён. | Исправьте поля и создайте новый заказ. |
| out_of_stock | Товар закончился до выполнения заказа. | Повторите позже. |
| product_unavailable | Во время выполнения товар стал недоступен. | Предложите другой товар. |
| maintenance | Приём заказов был приостановлен на короткое обслуживание. | Повторите через несколько минут. |
| fulfillment_failed | Заказ не удалось выполнить. | Повторите один раз; если повторится — напишите нам. |
| refunded | Заказ возвращён нашей командой. |
Получение заказов
Фильтры списка: status, product, reference, from, to (даты ISO), page, per_page. Сначала новые.
Опрос: проверяйте каждые 2–5 секунд, пока processing; остановитесь после финального статуса. Вебхуки избавляют от опроса.
Транзакции
Каждое движение баланса, сначала новые, с балансом после него. Типы: 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)
function oq_verify(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(",", "&", $header), $p);
if (!isset($p["t"], $p["v1"]) || abs(time() - (int)$p["t"]) > 300) return false;
$expected = hash_hmac("sha256", $p["t"] . "." . $rawBody, $secret);
return hash_equals($expected, $p["v1"]);
}
// $ok = oq_verify(file_get_contents("php://input"), $_SERVER["HTTP_X_OQ_SIGNATURE"], $secret);
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const p = Object.fromEntries(header.split(",").map(x => x.split("=")));
if (!p.t || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${p.t}.`).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1 || ""));
}
// Express: app.post("/oq/webhook", express.raw({type: "application/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, сайты, дневной лимит, только чтение). |