Заказы: создание, отслеживание и все возможные финалы
Один эндпоинт создаёт заказы, два читают их обратно. Интересен здесь не сам вызов, а набор состояний, в которые заказ может прийти, и то, под какие из них вам придётся писать код.
Создание заказа
POST /b2b/orders X-Api-Key: gc_live_… X-Idempotency-Key: 7f3c1a9e-2b44-4d1a-9d4e-1c0f9a2b3c4d Content-Type: application/json
{"items": [{ "productId": 34521, "quantity": 1, "deliveryData": { "gameUserId": "5123456789" } }],"externalOrderId": "MP-98765","callbackUrl": "https://api.yourshop.com/webhooks/gamecore"}
| Поле | Обязательное | Правила |
|---|---|---|
items | да | От 1 до 200 записей. Пустой массив — 400. |
items[].productId | да | Числовой id из каталога. |
items[].quantity | нет | По умолчанию 1; целые единицы, от 1 до 10000. |
items[].deliveryData | да | Отображение строка-в-строку. Если товару поля не нужны, передавайте {} — сам ключ не опциональный. |
externalOrderId | нет | Ваша собственная ссылка. Возвращается на обоих читающих эндпоинтах и в вебхуке — но не в ответе на создание, где приходят только коды. Сопоставление сохраняйте у себя в момент создания. |
callbackUrl | нет | Куда придёт вебхук. Проверяется при создании. |
X-Idempotency-Key обязателен: без него придёт 400. Эта проверка — первое действие хендлера, а значит, она идёт после аутентификации вашего ключа и после валидации тела по схеме: вызов без ключа получит 401, а кривое тело — 422, что бы ни было в заголовке. Поля цены нет: цену мы считаем в этот момент, по текущему курсу и вашему тарифу за объём — он применяется здесь и нигде в каталоге.
Всё, что происходит дальше, — одна транзакция базы: списание с баланса, запись платежа и заказы коммитятся вместе или не коммитятся вовсе. Состояния «деньги ушли, а заказа нет» не существует.
Один запрос — несколько заказов
{"success": true,"data": {"paymentCode": "P-AB12CD","totalAmount": 2129.0,"orders": [{ "code": "ash-XY7K3M", "gameId": "pubg-mobile", "gameName": "PUBG Mobile", "total": 1064.5, "itemCount": 1 },{ "code": "ash-QW4N8P", "gameId": "free-fire", "gameName": "Free Fire", "total": 1064.5, "itemCount": 1 }]}}
Позиции группируются по игре поставщика и по самому поставщику — а не по игре в том виде, в каком вы видите её в каталоге, — и каждая группа становится отдельным заказом со своим кодом и своим жизненным циклом. Поэтому корзина из трёх игр даст минимум три заказа, которые выполняются и падают независимо друг от друга, а одна каталожная игра может дать больше одного заказа, если её SKU приходят из нескольких источников. Никогда не считайте, что один запрос — это один заказ, даже для одной игры.
Два следствия, которые лучше заложить сразу: в вашей модели данных нужна связь «один ваш заказ — много наших», а по массиву orders нужно итерироваться даже там, где вы ждёте одну запись.
Код заказа — это короткий префикс вашего аккаунта плюс шесть символов из алфавита без O, 0, 1, I и L: ash-XY7K3M. Его безопасно диктовать голосом. Сохраняйте каждый код — дальше всё делается через него.
Чем оплачивается заказ
Заказы оплачиваются с вашего B2B-баланса в одном из двух режимов, согласованных при подключении:
- Предоплата — списание должно уместиться в баланс.
- Овердрафт — баланс может уйти в минус в пределах кредитного лимита.
Если не умещается, ничего не создаётся, а вызов возвращает 402 с причиной: Insufficient balance или Exceeds credit limit (available: …) с остатком. Это бизнес-условия, а не ошибки в запросе: повторить тот же вызов с тем же идемпотентным ключом после пополнения — ровно правильный ход.
Тот же статус носит третье сообщение, означающее совсем другое. Concurrent balance modification говорит, что ваш баланс изменило что-то ещё — обычно другой ваш же параллельный вызов создания заказа — пока этот коммитился; транзакция откатилась, поэтому деньги не двигались и заказа нет. Пополнять нечего: сделайте короткую паузу и повторите тот же самый запрос с тем же ключом. Если создавать заказы по аккаунту по одному, такое встречается редко. Полная таблица повторов — на странице Идемпотентность и ошибки.
Статусы заказа
| Статус | Что означает | Терминальный |
|---|---|---|
pending | Создан, ещё не оплачен. Живёт миллисекунды внутри транзакции — обычно вы его не увидите. | нет |
processing | Оплачен и передан поставщику. Нормальный ответ сразу после создания. | нет |
completed | Все позиции заказа доставлены. | да |
failed | Заказ не выполнился — включая случай, когда выполнилась только часть позиций. | да |
Ещё два значения, cancelled и refunded, существуют в самой колонке, но через этот API недостижимы: эндпоинта отмены нет, инициировать возврат партнёр не может. Обрабатывать их на всякий случай можно, но строить на их ожидании логику не нужно.
Статусы позиций
Статус позиции отдельный от статуса заказа, и словарь у него шире:
| Статус позиции | Что означает |
|---|---|
pending | Принята, ещё не разрешена. Это же вы видите, пока позиция ждёт очереди повторов. |
queued | Отложена в нашу очередь повторов, потому что поставщик был недоступен. |
awaiting_code | Ожидает код, приходящий вне канала API. |
awaiting_screenshot | Ожидает ручного подтверждения. |
completed | Доставлена. Именно здесь заполняется cdKeys, если у товара вообще есть ключи. |
failed | Не доставлена. Причину объясняют errorCode и errorMessage. |
Любой незнакомый статус позиции трактуйте как «ещё в работе». Код, который ветвится только на completed и failed, будет вечно ждать на awaiting_code, и это ожидание невидимо до жалобы покупателя.
Чтение одного заказа
GET /b2b/orders/ash-XY7K3M X-Api-Key: gc_live_…
{"success": true,"data": {"code": "ash-XY7K3M","externalOrderId": "MP-98765","gameId": "pubg-mobile","gameName": "PUBG Mobile","status": "completed","totalAmount": 1064.5,"createdAt": "2026-08-18T18:30:58.114Z","completedAt": "2026-08-18T18:31:15.432Z","items": [{"id": 991204,"productName": "PUBG Mobile 660 UC","amount": 1,"price": 1064.5,"status": "completed","cdKeys": [],"errorCode": null,"errorMessage": null,"completedAt": "2026-08-18T18:31:15.432Z"}]}}
Неизвестный код отдаёт 404 с телом { "success": false, "error": "Order not found" } — и ровно то же самое отдаёт код чужого партнёра: поиск идёт в пределах вашего аккаунта.
cdKeys возвращается только для позиций в статусе completed. В любом другом статусе там [], что означает «ещё нет», а не «не будет». Для товара id_only он останется [] навсегда, и это правильное финальное состояние.
Список заказов
GET /b2b/orders?status=failed&page=1&limit=100
| Параметр | По умолчанию | Примечания |
|---|---|---|
page | 1 | Нумерация с единицы. |
limit | 20 | Ограничен сверху 100. |
status | — | Точное совпадение со статусом заказа. Неизвестное значение вернёт пустую страницу, а не ошибку. |
Сортировка фиксированная: сначала новые по времени создания. В ответе есть pagination с page, limit и total, а каждая строка — это заказ без позиций; за позициями идите в запрос одного заказа.
Этот эндпоинт нужен для сверки, а не для доставки. Он правильный инструмент для «покажи всё, что упало за последний час», и неправильный — чтобы узнать, выполнился ли один конкретный заказ.
Частичная доставка
Заказ из нескольких позиций может закончиться так, что часть доставлена, а часть нет. Что происходит дальше — самая неочевидная часть этого API, поэтому проговорим её по пунктам:
- Статус заказа становится
failed, а не «частично выполнен». Частичного статуса не существует. Доставленные позиции при этом остаются доставленными и лежат в ответе соstatus: "completed". - Вебхук не отправляется. Вебхуки эмитятся, когда выполнились все позиции или когда провалились все. Смешанный исход не эмитит ничего: если вы полагаетесь только на вебхуки, частичный заказ пройдёт молча.
- Проваленные строки автоматически возвращаются на баланс. Стоимость доставленных строк — нет: товар у вас, деньги за него списаны.
Практическое правило: сверка не опциональна. Опрашивайте заказы, которые через несколько минут всё ещё в processing, и всегда читайте массив позиций, а не только статус заказа: failed на уровне заказа вполне может означать, что покупатель получил бо́льшую часть корзины.
Когда заказ не двигается
Две разные ситуации выглядят снаружи одинаково, и разрешается сама собой только одна.
Поставщик недоступен. Позиция уходит в очередь повторов и повторяется по лестнице примерно 1 минута, 5 минут, 15 минут, 1 час и 6 часов, до десяти попыток. Если очередь сдаётся, позиция помечается failed, заказ разрешается, а деньги возвращаются на ваш баланс. Всё это время вы видите только processing.
Поставщик принял заказ и не разрешает его. Мы опрашиваем его примерно раз в минуту столько, сколько потребуется. Часовой таймаут, который сам провалил бы такой заказ, отсутствует: заказ остаётся в processing неограниченно долго, и деньги автоматически не возвращаются.
Поэтому заказ, висящий в processing часами, не нужно продолжать опрашивать. Заведите на него алерт — для автоматических типов доставки порог в час разумен — и напишите нам с кодом заказа. Ручные типы доставки законно идут дольше; порог для них задайте отдельно.
Поллинг или вебхуки
Основным каналом делайте вебхуки, а поллинг — страховкой. По отдельности не хватает ни того, ни другого: вебхуки молчат на частичных заказах, а поллинг при любом здравом интервале медленнее пуша.
Рабочая схема:
- Указывайте
callbackUrlв каждом заказе и реагируйте на событие, когда оно пришло. - Раз в несколько минут проходите по своим ещё открытым заказам старше пары минут и перечитывайте каждый.
- Не опрашивайте один заказ в плотном цикле. Его статус не может обновиться быстрее нашего собственного опроса поставщика — примерно раз в минуту, — а бюджет у вас 500 запросов в минуту на всю интеграцию.
- Сделайте оба пути идемпотентными друг относительно друга. Один и тот же заказ регулярно будет разрешаться дважды — вебхуком и сверкой, — и оба раза это должно быть безопасно.
Перед запуском
- В вашей модели один ваш заказ связан со многими нашими по полю
code. failedна уровне заказа разбирается по позициям до того, как вы что-то скажете покупателю.- Частичные заказы находит ваша сверка, потому что вебхук о них не сообщит.
- Незнакомые статусы позиций считаются «в работе».
cdKeys: []у выполненной позицииid_only— это успешный путь, а не ошибочный.- Заказ, застрявший в
processingдольше вашего порога, поднимает алерт, а не новый цикл опроса. 402по балансу повторяется после пополнения тем же идемпотентным ключом, а не новым.
Обновлено 18 августа 2026 г.
Смотрите также: Поставка по API · Цены и тарифы
Нужен ключ API?
Напишите, какие игры и регионы вы продаёте, — выдадим ключ и пройдём интеграцию вместе с вами. На вопросы по интеграции отвечают те же люди, которые ведут API.