REST API — v1
API пополнения игр и ваучеров для реселлеров
Один REST API для пополнения игр, игровых счетов и кодов ваучеров: читайте каталог, проверяйте игрока, создавайте заказ и забирайте результат вебхуком или запросом статуса.
Это host-to-host (H2H) API пополнений, на котором работает SHOP2TOPUP. Обычный JSON API поверх HTTPS с Bearer-аутентификацией, ключами идемпотентности на UUID, опубликованными лимитами запросов и стабильными машиночитаемыми кодами ошибок — магазин, витрина или бот могут продавать цифровую игровую валюту программно, без клиентской библиотеки. Через этот же интерфейс прошло 3 000 000+ доставленных заказов за 5+ лет непрерывной работы.
Ищете цены, способы оплаты и описание самого партнёрского аккаунта? Прочитайте обзор реселлерской программы SHOP2TOPUP.
Первый вызов — ещё до всяких подписаний
Каждый эндпоинт — это обычный HTTPS-запрос с одним заголовком. Нечего устанавливать, нечего добавлять в зависимости и нечего дописывать в сборку. Вставьте вызов ниже, подставьте свой ключ — и живой ответ каталога уже в вашем терминале.
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
-H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"{
"success": true,
"categories": [
{
"id": 12,
"name": "Free Fire",
"description": "Garena Free Fire diamonds",
"big_category_id": 1,
"big_category_name": "Mobile Games"
}
]
}Базовый URL
https://shop2topup.com/api/endpoints/v1
Все задокументированные пути указаны относительно этого базового URL. Ответы приходят в JSON и всегда содержат булево поле success, поэтому одна ветка в клиенте отделяет успешный сценарий от всего остального.
Конверт ошибки
Ошибки сохраняют одинаковую форму при любом статусе. Ветвитесь по error.code — он часть контракта; никогда не ветвитесь по error.message, который написан для людей и может быть переформулирован в любой момент.
{
"success": false,
"error": {
"code": "PRICE_INCREASED",
"message": "Price has increased beyond expected",
"details": {
"expected_unit_price": "0.950000",
"current_unit_price": "0.980000"
}
}
}Вся поверхность API на одном экране
Одиннадцать эндпоинтов покрывают всю интеграцию: прочитать каталог, узнать цену товара, проверить игрока, оформить заказ и потом свести его. Каждая строка ведёт прямо в полный справочник — со всеми параметрами, примером запроса и примером ответа.
| Метод | Путь | Что делает | Справочник |
|---|---|---|---|
| GET | /account | Get Account Info | Открыть справочник |
| GET | /catalog/big-categories | List Big Categories | Открыть справочник |
| GET | /catalog/categories | List Categories | Открыть справочник |
| GET | /catalog/subcategories | List Subcategories (Products) | Открыть справочник |
| GET | /catalog/subcategory/:itemId/price | Get Item Price | Открыть справочник |
| GET | /catalog/category/:categoryId/requirements | Get Category Requirements | Открыть справочник |
| POST | /player/validate | Validate Player | Открыть справочник |
| POST | /orders/create | Create Order | Открыть справочник |
| GET | /orders/:orderId | Get Order Status | Открыть справочник |
| POST | /orders/batch | Batch Get Orders | Открыть справочник |
| GET | /orders | List Orders | Открыть справочник |
Плюс вебхуки статуса заказа — они полностью описаны ниже на этой странице.
Аутентификация и контроль доступа
Аутентификация — это один HTTP-заголовок. Каждый запрос несёт заголовок Authorization с Bearer-учёткой из идентификатора ключа и секрета, выпущенных на странице API Access в панели реселлера. Идентификатор ключа определяет аккаунт, секрет доказывает, что он ваш, и проверяется на сервере HMAC-проверкой.
Секрет показывается один раз, в момент создания, и потом его нельзя получить снова. Положите его в тот менеджер секретов, который уже есть в вашем стеке, прокидывайте переменной окружения и держите вне системы контроля версий. Потеряли — не восстанавливаете, а ротируете.
Нет ни редиректа OAuth, ни сессионной куки, ни refresh-токена, и планировать нечего. Для автоматизированных интеграций это важно: cron-задача или воркер очереди могут годами держать одну и ту же учётку без пути обновления, и нет счётчика истечения, из-за которого приходится вставать в три часа ночи.
К ключу можно прикрепить список разрешённых IP. Как только он непустой, запросы с любых других адресов отклоняются с IP_NOT_ALLOWED и HTTP 403 ещё до выполнения бизнес-логики, поэтому утёкшая учётка бесполезна вне ваших серверов. Ротация — это деплой, а не миграция: создайте новый ключ, выкатите, удалите старый.
Ключи принадлежат одному аккаунту реселлера и тратят кошелёк этого аккаунта, поэтому никогда не отправляйте секрет в браузер, в мобильную сборку или в публичный репозиторий. Вызывайте API со своего бэкенда, а фронтенд пусть общается с вашим бэкендом. Всё описанное здесь рассчитано на вызывающую сторону server-to-server.
Authorization: Bearer <keyId>.<secret>Проверка ключа одним вызовом
GET /account — это health-check для учётки. Он сразу отвечает на три вопроса: проходит ли ключ аутентификацию, включён ли аккаунт и хватает ли на кошельке средств на то, что вы собираетесь заказать. Вызывайте его при старте и перед любым пакетным прогоном.
Открыть справочник по аутентификации и аккаунтуПолная интеграция от начала до конца
Рабочая интеграция — это четыре шага. Примеры ниже проходят их все на живом API: на cURL, Node и PHP. Подставьте свою учётку — и они выполнятся как есть.
- 1
Прочитайте каталог
Пройдите от больших категорий к категориям и подкатегориям, затем узнайте текущую цену товара, который собираетесь продать. Дерево кэшируйте, цену запрашивайте заново.
- 2
Проверьте игрока
Сопоставьте идентификатор с игровым именем и регионом до того, как пойдут деньги. Именно здесь ловятся опечатки и ошибки с чужим регионом.
- 3
Создайте заказ
Сгенерируйте UUID, сохраните его и отправьте как order_id. Это ваш ключ идемпотентности и единственный безопасный способ повторить запрос на создание.
- 4
Подтвердите доставку
Дождитесь вебхука со статусом заказа, а для всего, что осталось в pending после вашего окна ожидания, используйте чтение статуса.
cURL — полный сценарийПоказать кодСкрыть код
export S2T_KEY="YOUR_KEY_ID.YOUR_KEY_SECRET"
export S2T_BASE="https://shop2topup.com/api/endpoints/v1"
# 1. Find the item you want to sell.
curl -s "$S2T_BASE/catalog/subcategories?categoryId=12" \
-H "Authorization: Bearer $S2T_KEY"
# 2. Read the exact price you will be charged for it.
curl -s "$S2T_BASE/catalog/subcategory/999/price" \
-H "Authorization: Bearer $S2T_KEY"
# 3. Confirm the player exists BEFORE any money moves.
curl -s -X POST "$S2T_BASE/player/validate" \
-H "Authorization: Bearer $S2T_KEY" \
-H "Content-Type: application/json" \
-d '{"sub_category_id": 999, "player_id": "123456789", "server": "Asia"}'
# 4. Create the order. order_id is YOUR uuid and YOUR idempotency key.
curl -s -X POST "$S2T_BASE/orders/create" \
-H "Authorization: Bearer $S2T_KEY" \
-H "Content-Type: application/json" \
-d '{
"order_id": "01912345-6789-7abc-8def-0123456789ab",
"sub_category_id": 999,
"quantity": 1,
"requirements": { "player_id": "123456789", "server": "Asia" },
"expected_unit_price": "0.950000"
}'
# 5. Read the order back until it leaves "pending".
curl -s "$S2T_BASE/orders/01912345-6789-7abc-8def-0123456789ab" \
-H "Authorization: Bearer $S2T_KEY"Node.js — полный сценарийПоказать кодСкрыть код
// Node 18+ — no dependencies, global fetch and global crypto.
const BASE = 'https://shop2topup.com/api/endpoints/v1';
const KEY = process.env.S2T_KEY; // "<keyId>.<secret>", server-side only.
async function call(path, init = {}) {
const res = await fetch(BASE + path, {
...init,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
...(init.headers || {}),
},
});
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') || 5);
await new Promise((r) => setTimeout(r, wait * 1000));
return call(path, init);
}
const body = await res.json();
if (!res.ok || body.success === false) {
throw Object.assign(new Error(body?.error?.code || 'HTTP_' + res.status), {
code: body?.error?.code,
status: res.status,
});
}
return body;
}
async function sell({ itemId, categoryId, playerId, server }) {
// 1 + 2. Catalog and current price.
const { subcategories } = await call(
`/catalog/subcategories?categoryId=${categoryId}`,
);
const item = subcategories.find((s) => s.item_id === itemId);
const { price } = await call(`/catalog/subcategory/${itemId}/price`);
// 3. Player check before charging.
const { player } = await call('/player/validate', {
method: 'POST',
body: JSON.stringify({
sub_category_id: itemId,
player_id: playerId,
server,
}),
});
// 4. Create the order. Persist orderId BEFORE the call so a crash mid-flight
// can be replayed with the same uuid instead of double-charging.
const orderId = crypto.randomUUID();
await saveIntent(orderId, itemId, playerId);
const { order } = await call('/orders/create', {
method: 'POST',
body: JSON.stringify({
order_id: orderId,
sub_category_id: itemId,
quantity: 1,
requirements: { player_id: playerId, server },
expected_unit_price: price.unit_price,
}),
});
// 5. Confirm. A webhook usually beats the poll; the poll is the safety net.
let status = order.status;
while (status === 'pending') {
await new Promise((r) => setTimeout(r, 20000));
status = (await call(`/orders/${orderId}`)).order.status;
}
return { orderId, status, itemName: item?.name, playerName: player.player_name };
}PHP — полный сценарийПоказать кодСкрыть код
<?php
// PHP 8 — plain cURL, no SDK required.
const S2T_BASE = 'https://shop2topup.com/api/endpoints/v1';
function s2t(string $path, ?array $body = null): array
{
$ch = curl_init(S2T_BASE . $path);
$headers = [
'Authorization: Bearer ' . getenv('S2T_KEY'),
'Content-Type: application/json',
];
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true) ?: [];
if ($status >= 400 || ($data['success'] ?? false) === false) {
throw new RuntimeException($data['error']['code'] ?? 'HTTP_' . $status);
}
return $data;
}
// 1 + 2. Catalog, then the exact price for the item.
$catalog = s2t('/catalog/subcategories?categoryId=12');
$price = s2t('/catalog/subcategory/999/price');
// 3. Player check before charging.
$player = s2t('/player/validate', [
'sub_category_id' => 999,
'player_id' => '123456789',
'server' => 'Asia',
]);
// 4. Create the order with your own uuid as the idempotency key.
$orderId = sprintf(
'%04x%04x-%04x-7%03x-%04x-%04x%04x%04x',
random_int(0, 0xffff), random_int(0, 0xffff),
random_int(0, 0xffff), random_int(0, 0x0fff),
random_int(0x8000, 0xbfff),
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);
$order = s2t('/orders/create', [
'order_id' => $orderId,
'sub_category_id' => 999,
'quantity' => 1,
'requirements' => ['player_id' => '123456789', 'server' => 'Asia'],
'expected_unit_price' => $price['price']['unit_price'],
]);
// 5. Read it back until it settles.
do {
sleep(20);
$latest = s2t('/orders/' . $orderId);
} while ($latest['order']['status'] === 'pending');
echo $latest['order']['status'];Две детали стоит скопировать дословно: сохраняйте UUID заказа до вызова создания, а не после, и перечитывайте цену, а не доверяйте закэшированному числу. Эти две привычки убирают почти все двойные списания и почти все отказы PRICE_INCREASED.
Вебхуки: колбэки со статусом заказа
Зарегистрируйте один HTTPS-эндпоинт — и платформа будет присылать на него результаты заказов по мере их появления, чтобы витрина перестала опрашивать и начала реагировать. Колбэк подписан, поэтому вы можете доказать, что тело пришло от нас, прежде чем что-то с ним делать.
События
| Событие | Когда срабатывает |
|---|---|
| order.completed | The order finished and everything it owed the buyer was delivered. |
| order.failed | The order failed and no sub-transaction is left pending or processing. |
| order.refunded | The order was fully refunded back to your wallet. |
| webhook.test | You triggered a signed test delivery from the panel to check your handler. |
Заголовки доставки
| Заголовок | Значение |
|---|---|
| Content-Type | application/json |
| X-Shop2Topup-Signature | sha256=<hex> — HMAC-SHA256 of the raw request body |
| X-Shop2Topup-Event | Event name, e.g. order.completed |
| User-Agent | shop2topup-webhook/1.0 |
Тело колбэка
У каждого колбэка одни и те же три поля верхнего уровня — event, timestamp и data, — а data повторяет объект заказа, который возвращает эндпоинт статуса. Коды ваучеров есть только у выполненного заказа, player_name может быть null, а sub_transaction_summary появляется, когда заказ был разбит на единицы.
{
"event": "order.completed",
"timestamp": "2026-03-11T14:30:00.000Z",
"data": {
"order_id": "01912345-6789-7abc-8def-0123456789ab",
"status": "completed",
"player_id": "123456789",
"player_name": "ProGamer99",
"subcategory_name": "FF 100 Diamonds",
"quantity": 1,
"charged_amount": "0.950000",
"currency": "USD",
"created_at": "2026-03-11T14:28:00.000Z",
"completed_at": "2026-03-11T14:30:00.000Z",
"vouchers": [
{
"code": "XXXX-YYYY-ZZZZ",
"serial_number": "SN-9928371",
"expiry_date": "2027-03-11"
}
],
"sub_transaction_summary": {
"total": 1,
"completed": 1,
"failed": 0,
"pending": 0,
"processing": 0,
"retrying": 0,
"refunded": 0
}
}
}Проверка подписи
Каждая доставка несёт X-Shop2Topup-Signature вида sha256= и далее hex HMAC-SHA256 от сырого тела запроса на вашем webhook-секрете. Проверяйте по сырым байтам, до любого разбора JSON, и сравнивайте за константное время. Обработчик, который сначала парсит, а потом проверяет, можно обмануть пересобранным телом.
// Express — verify the signature against the RAW body, not the parsed object.
const crypto = require('crypto');
app.post(
'/shop2topup/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.get('X-Shop2Topup-Signature') || '';
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.S2T_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const payload = JSON.parse(req.body.toString('utf8'));
// Answer fast, then do the work. The delivery times out after 10 seconds.
res.status(200).end();
enqueueSettlement(payload.data.order_id, payload.event);
},
);Доставка и повторы
Колбэк отправляется один раз, и у вашего эндпоинта есть десять секунд на ответ. Автоматических повторов и экспоненциальной задержки за этим нет, и это сделано намеренно: авторитетная запись — эндпоинт статуса, поэтому сверка принадлежит опросу, которым управляете вы, а не расписанию повторов, которого вы не видите. Возвращайте 200 сразу, а работу выполняйте в своей очереди.
Идемпотентные обработчики
Стройте обработчик вокруг order_id и делайте повтор колбэка холостым. Даже при одной попытке доставки ваша собственная инфраструктура иногда отдаст одно и то же сообщение двум воркерам, а заказ, который зачислил клиенту дважды, — гораздо худший баг, чем заказ, зачисленный с опозданием.
Регистрация и секреты
Регистрация URL колбэка, отправка подписанной тестовой доставки и ротация секрета подписи выполняются в панели реселлера под вашей учётной записью, а не через публичный API. HTTPS обязателен, приватные адреса не принимаются.
Открыть API access в панели реселлераЛимиты запросов и обработка ошибок
Лимиты опубликованы, считаются на аккаунт и применяются по скользящему окну. Самый широкий запас — у работы с каталогом; эндпоинты статуса намеренно узкие, потому что задуманы как запасной вариант для сверки, а не как цикл опроса.
| Маршрут | Запросов | Окно | Считается на |
|---|---|---|---|
| GET /catalog/* (all five) | 80 | 60s | per account |
| POST /orders/create | 120 | 60s | per account |
| GET /orders/:orderId | 3 | 60s | per account + order id |
| POST /orders/batch | 2 | 60s | per account |
| GET /orders | 2 | 60s | per account |
| GET /account | 60 | 60s | per account |
| POST /player/validate | dedicated limiter | — | per account |
Каждый ответ несёт X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset, поэтому корректному клиенту не приходится гадать, где он находится. Следите за счётчиком remaining и притормаживайте до того, как вас ограничат, а не после.
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1765000000
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 45 seconds.",
"details": {
"limit": 80,
"remaining": 0,
"retry_after": 45,
"window_seconds": 60
}
}
}Ошибки, с которыми вы столкнётесь первыми, и их решения
Отказы всегда возвращают стабильный код рядом с HTTP-статусом. Здесь собраны те, на которые новая интеграция натыкается в первую же неделю, и к каждому приложено изменение, которое его снимает.
| Код | HTTP | Что означает | Как исправить |
|---|---|---|---|
| INVALID_API_KEY | 401 | Invalid API keyThe provided API key does not match any active key in the system. | Verify your key ID and secret are correct. Regenerate the key from the API Access panel if needed. |
| IP_NOT_ALLOWED | 403 | IP address not in allowlistThe request originated from an IP address not in your API key allowlist. | Add your server IP to the allowlist in the API Access panel, or remove IP restrictions. |
| MISSING_REQUIRED_FIELD | 400 | Missing required fieldA required field is missing from the request body. | Check the endpoint documentation for required fields. |
| INVALID_UUID_FORMAT | 400 | Invalid UUID formatThe order_id is not a valid UUID format. | Use any valid UUID format (v1, v4, v7, etc). Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. |
| PLAYER_NOT_FOUND | 400 | Player not foundThe player ID could not be found in the game system. | Verify the player ID is correct. Check if a zone_id is required for the game. |
| REGION_MISMATCH | 400 | Region mismatchThe player belongs to a different region than expected. | Use the correct zone_id or genshin_zone for the player region. |
| INSUFFICIENT_BALANCE | 400 | Insufficient wallet balanceYour wallet does not have enough funds to complete this order. | Top up your wallet from the Reload page before placing the order. |
| OUT_OF_STOCK | 400 | Product is out of stockThe requested product is currently unavailable. | Try again later or choose a different product. |
| DUPLICATE_ORDER | 409 | Duplicate order IDAn order with this UUID already exists. This is an idempotency protection. | Generate a new UUID for a new order. If retrying, use the same UUID to get the existing order. |
| PRICE_INCREASED | 400 | Price has increased beyond expectedThe current unit price is higher than the expected_unit_price you provided. | Fetch the latest price with GET /catalog/subcategory/:id/price and update expected_unit_price. |
| RATE_LIMIT_EXCEEDED | 429 | Rate limit exceededYou have exceeded the rate limit for this endpoint. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. When exceeded, the 429 response body contains: limit, remaining (0), retry_after (seconds to wait), and window_seconds. A Retry-After header is also set. | Wait for the number of seconds indicated by retry_after or the Retry-After header before retrying. Monitor X-RateLimit-Remaining headers to avoid hitting limits. |
| SERVICE_UNAVAILABLE | 503 | Service temporarily unavailableThe service is temporarily down for maintenance or overloaded. | Wait and retry after a few minutes. |
Ответы на вопросы по интеграции
Вопросы, которые разработчики действительно задают при подключении: аутентификация, повторы, идемпотентность, лимиты, колбэки и обработка сбоев.
Как реселлерский API аутентифицирует запросы?
Одним HTTP-заголовком: Authorization: Bearer <keyId>.<secret>. Пара ключей выпускается на странице API Access в панели реселлера, секрет показывается один раз при создании, и сервер проверяет его HMAC-проверкой на каждом вызове. Нет ни редиректа OAuth, ни сессионной куки, ни токена, который нужно обновлять, — один и тот же заголовок работает и для первого запроса, и для всех последующих.
На какой базовый URL и версию должен ходить мой клиент?
На один базовый URL, и он боевой: отдельного песочного хоста, на котором сначала пишут, а потом переключаются, здесь нет. Направьте клиент на базовый URL v1, показанный выше, отправляйте Authorization: Bearer <keyId>.<secret> в каждом запросе и начните с GET /account, чтобы убедиться в работоспособности ключа и формате заголовка. POST /orders/create — единственный вызов, который двигает деньги: оставьте его напоследок, запускайте на самом мелком номинале и отправляйте вместе с ним expected_unit_price, пока клиент ещё обкатывается.
Какое поле является ключом идемпотентности в POST /orders/create?
Поле order_id — UUID, который вы генерируете у себя и отправляете в теле создания. Повторное создание с уже существующим order_id получает HTTP 409 и код DUPLICATE_ORDER вместо второго заказа, поэтому повтор запроса не превращается во второе списание; исходный заказ читается через GET /orders/:orderId. Сгенерируйте и сохраните UUID до первой попытки: UUID, созданный после таймаута, — это новый ключ, а не ключ повтора, и сервер не склеит два order_id, которыми вы имели в виду одно и то же.
Запрос завершился таймаутом. Заказ создан?
Выясните это чтением статуса, а не догадками. Вызовите GET /orders/:orderId с тем же UUID, что отправляли: если заказ вернулся, таймаут случился уже после его приёма и повторять запрос не нужно. Если пришёл ORDER_NOT_FOUND, ничего не списано и запрос на создание с тем же UUID можно безопасно повторить.
Как проверить подпись у вебхук-колбэка?
Пересчитайте HMAC по сырому телу запроса и сравните с заголовком X-Shop2Topup-Signature. В заголовке лежит sha256=<hex>: посчитайте HMAC-SHA256 по тем самым байтам, которые пришли, своим секретом вебхука, добавьте префикс sha256= и сравнивайте за константное время — но никогда не по повторно сериализованному JSON-объекту, потому что разбор и обратная сборка меняют байты, и совпадения не будет никогда. Несовпадение отклоняйте с 401, на корректную доставку отвечайте 200 в пределах десяти секунд, а сверку выполняйте потом в своей очереди.
Что происходит при превышении лимита запросов?
Вы получаете HTTP 429 с кодом RATE_LIMIT_EXCEEDED и заголовком Retry-After. В теле ответа то же число повторяется в retry_after, плюс приходят limit, remaining и window_seconds. Подождите ровно столько секунд и повторите один раз; не повторяйте сразу и не размазывайте один и тот же вызов по нескольким ключам — счётчик привязан к аккаунту, а не к соединению.
Какие поля возвращает POST /player/validate?
Поле player_name, а также zone_id и region, если игра их отдаёт, — внутри объекта data в ответе. Создание заказа само разрешает игрока и проверяет регион, а попытка из чужого региона отклоняется кодом REGION_MISMATCH (HTTP 400) до какого-либо списания с кошелька, так что баланс защищает не этот вызов. Вызывайте его потому, что прочитанный player_name — единственный практичный способ показать покупателю аккаунт, который он собирается пополнить, и поймать опечатку в идентификаторе до подтверждения: имя всегда приходит из игры, а не из того, что ввёл покупатель.
Как превратить ответ requirements в тело заказа?
Разложите его по field_name: каждая запись, которую возвращает GET /catalog/category/:categoryId/requirements, становится одним свойством объекта requirements, отправляемого в POST /orders/create. В каждой записи есть data_type — форма знает, что рисовать, а сервер знает, к какому типу приводить; записи single_select и multi_select дополнительно везут список допустимых значений, и предлагать нужно именно его, а не свободный ввод. Кешируйте список по категории вместо чтения на каждый заказ и перечитывайте его, когда создание возвращает MISSING_REQUIRED_FIELD.
Как клиенту обрабатывать ответ PRICE_INCREASED?
Перечитать цену, принять решение и отправить снова — но не повторять то же тело. PRICE_INCREASED (HTTP 400) означает, что текущая цена за единицу выше отправленного вами expected_unit_price, поэтому создание отклонено и ничего не списано; в details ошибки лежат и expected_unit_price, и current_unit_price, а слепой повтор со старым числом будет отклоняться каждый раз. Прочитайте GET /catalog/subcategory/:itemId/price, решите, устраивает ли вас новое число, и отправьте создание снова с обновлённым expected_unit_price и тем же order_id — отклонённая попытка его не израсходовала. Если expected_unit_price не отправлять, защита выключается совсем, а автоматической интеграции это почти никогда не нужно.
Как частично доставленный заказ выглядит в ответе?
Как статус partial, а разбивка лежит в sub_transaction_summary. Этот объект считает единицы по состояниям — completed, refunded, failed, pending, processing, retrying, — а sub_transactions отдаёт строку за каждым счётчиком, так что ваша логика сверки читает числа, а не догадывается по одному слову. Считайте pending и processing нетерминальными и продолжайте ждать; completed, partial, failed и refunded — терминальные. Колбэки order.completed, order.failed и order.refunded несут ту же сводку, поэтому payload с проверенной подписью закрывает заказ без повторного чтения.
Что получит запрос с адреса вне списка разрешённых?
HTTP 403 с кодом IP_NOT_ALLOWED — ответ приходит до любой бизнес-логики. Проверка работает, пока список разрешённых IP у ключа непустой, поэтому утёкший ключ бесполезен где-либо, кроме ваших собственных исходящих адресов. Ветвитесь по error.code, а не по тексту сообщения, и внезапный IP_NOT_ALLOWED в проде читайте как смену исходящего адреса — новый NAT-шлюз или новая подсеть воркеров, — а не как сломанный ключ. Оставляйте список пустым только если исходящий адрес действительно нестабилен.
Сколько времени занимает выполнение заказа?
Считайте выполнение асинхронным, а не мгновенным. POST /orders/create отвечает, как только заказ принят и деньги списаны с кошелька, — обычно со статусом pending; дальше исполнение идёт на отдельных воркерах, а терминальное состояние приходит к вам вебхуком или при следующем чтении статуса. Не блокируйте клиентский HTTP-запрос ожиданием финального состояния: примите заказ, ответьте покупателю и завершите расчёт вне основного потока.
Создайте аккаунт, сгенерируйте ключ, запускайтесь
Между регистрацией и первым вызовом API нет ни очереди на одобрение, ни периода ожидания. Создайте аккаунт реселлера, сгенерируйте ключ на странице API Access, пополните кошелёк — и первый заказ может уйти в тот же день.
Вы не разработчик этого проекта? Коммерческая сторона описана на странице реселлерской программы SHOP2TOPUP.