# DBD Merchant API — полная документация Base URL: `https://merchant.dbdlobby.com`. Только HTTPS и JSON. Авторизация — заголовок `X-Entity-Key`. Цены — числом в валюте товара (RUB или USD), время — ISO 8601 в UTC. Веб-версия: https://docs.merchant.dbdlobby.com/ru. OpenAPI: https://docs.merchant.dbdlobby.com/openapi.json. # Руководства ## Аутентификация Каждый запрос, кроме /health/api, подписывается ключом клиента в заголовке X-Entity-Key. Ключ выдаём мы при подключении. У нас хранится только его хэш (SHA-256), поэтому восстановить потерянный ключ нельзя — только выпустить новый. ```bash curl https://merchant.dbdlobby.com/products \ -H "X-Entity-Key: YOUR_ENTITY_KEY" ``` > [!WARNING] > API рассчитан на вызовы с вашего сервера. Не встраивайте ключ в браузерный или мобильный код: с ним можно тратить ваш баланс. ### Ошибки авторизации | Код | message | Причина | |---|---|---| | 401 | `Missing X-Entity-Key header` | Заголовка нет или он пустой. | | 401 | `Invalid X-Entity-Key header` | Значение длиннее 128 символов. | | 403 | `Invalid entity key` | Такого ключа нет. | | 403 | `API access disabled for this client` | Доступ к API для клиента отключён. Свяжитесь с нами. | Песочница (`/sandbox/*`) принимает тот же ключ: отдельный тестовый ключ не нужен. ## Покупка и статусы заказа Как проходит покупка, когда списываются и возвращаются деньги, где взять ключ. ### Шаги 1. Перед покупкой можно проверить наличие: [GET /products/{id}/availability](https://docs.merchant.dbdlobby.com/ru/reference/product-availability). 2. [POST /purchase/{productId}](https://docs.merchant.dbdlobby.com/ru/reference/purchase) с `idempotencyKey`. Заказ создаётся, и стоимость сразу резервируется с баланса — в одной операции. Если денег не хватает, заказ не создаётся (`402`). 3. Товар с ручной выдачей завершается сразу. Товар с автоматической выдачей покупается у поставщика, после получения ключа заказ ещё короткое время проверяется и только потом становится `completed`. 4. Ключ приходит в поле `key` ответа покупки или [GET /orders/{id}](https://docs.merchant.dbdlobby.com/ru/reference/get-order). В вебхуке ключа нет. ### Статусы | status | Значение | Деньги | |---|---|---| | `pending` | Выполняется. Дождитесь вебхука или опрашивайте заказ раз в 5–10 секунд. | Зарезервированы | | `completed` | Выполнен, ключ доступен. | Списаны | | `failed` | Не выполнен, причина в `failureReason`. | Возвращены | | `cancelled` | Отменён поставщиком. | Возвращены | `completed`, `failed` и `cancelled` — окончательные: статус больше не меняется. ### Ответ 202 Если покупка у поставщика не уложилась примерно в 50 секунд, API отвечает `202` с заказом в статусе `pending`. Это не ошибка и не повод покупать снова: заказ продолжает выполняться. Узнайте исход через вебхук или `GET /orders/{id}`. В редких случаях, когда исход нужно перепроверить, заказ остаётся в `pending` дольше — деньги всё это время зарезервированы и вернутся, если заказ не будет выполнен. > [!NOTE] > Ставьте таймаут HTTP-клиента не меньше 90 секунд: покупка может длиться до ~50 секунд. ## Идемпотентность Как безопасно повторять покупку после таймаута или обрыва связи. Если ответ на `POST /purchase` не дошёл, вы не знаете, прошла ли покупка. Повтор без защиты — это вторая покупка и второе списание. Поэтому передавайте `idempotencyKey` — свой уникальный идентификатор покупки, например ID заказа в вашей системе. - Первый запрос с ключом создаёт заказ. - Повтор с тем же ключом и тем же товаром возвращает исход того же заказа — с тем же кодом ответа и полем `"replayed": true`. Деньги второй раз не списываются, даже если товар уже закончился. - Тот же ключ с другим товаром — `422 idempotency_key_reused`. - Если заказ не был создан (например, `402` — не хватило денег), ключ не занят: после пополнения можно повторить с ним же. ### Формат 1–128 печатных ASCII-символов (`!`…`~`), без пробелов и кириллицы. Регистр важен: `ABC` и `abc` — разные покупки. Ключ действует в пределах вашего клиента. ### Повтор с тем же ключом ```javascript async function buy(productId, idempotencyKey) { for (let attempt = 1; attempt <= 5; attempt++) { try { const res = await fetch(`https://merchant.dbdlobby.com/purchase/${productId}`, { method: "POST", headers: { "X-Entity-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ idempotencyKey }), // один и тот же ключ во всех попытках signal: AbortSignal.timeout(90_000), }); if (res.status >= 500 && res.status !== 502) throw new Error(`HTTP ${res.status}`); return { status: res.status, body: await res.json() }; } catch (e) { // Сеть, таймаут или 5xx: исход неизвестен — повторяем С ТЕМ ЖЕ ключом. await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); } } throw new Error("API недоступен; повторите позже с тем же idempotencyKey"); } ``` Ключ необязателен, но без него повтор после таймаута создаст вторую покупку. В будущем он может стать обязательным. ## Вебхуки Уведомление на ваш сервер, когда заказ получает окончательный статус. Передайте `webhookUrl` в теле `POST /purchase`. Когда заказ станет `completed`, `failed` или `cancelled`, мы отправим на этот адрес POST-запрос: ```json { "event": "order.status", "orderId": 1842, "status": "completed", "failureReason": null, "productId": 12, "price": 1490, "currency": "RUB", "idempotencyKey": "order-7731", "createdAt": "2026-11-13T12:36:53.000Z" } ``` Ключа товара в вебхуке нет — получите его через [GET /orders/{orderId}](https://docs.merchant.dbdlobby.com/ru/reference/get-order). ### Заголовки | Заголовок | Значение | |---|---| | `X-Merchant-Timestamp` | Время отправки, Unix-секунды. | | `X-Merchant-Signature` | `sha256=` + hex HMAC-SHA256 от строки `.<тело запроса>` на вашем секрете вебхуков. Секрет выдаём вместе с ключом доступа. | | `User-Agent` | `merchant-webhooks/1` | ### Проверка подписи Всегда проверяйте подпись и время: иначе кто угодно сможет прислать вам «оплаченный» заказ. ```javascript import crypto from "node:crypto"; // rawBody — тело запроса строкой, ДО JSON.parse: подпись считается по байтам. export function verifyMerchantWebhook(rawBody, headers, secret) { const timestamp = headers["x-merchant-timestamp"]; const signature = headers["x-merchant-signature"] ?? ""; // Отбрасываем старые доставки — защита от повторной отправки перехваченного запроса. if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"); const a = Buffer.from(signature); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b); } // Express: // app.post("/hooks/merchant", express.raw({ type: "application/json" }), (req, res) => { // if (!verifyMerchantWebhook(req.body.toString("utf8"), req.headers, process.env.MERCHANT_WEBHOOK_SECRET)) // return res.sendStatus(401); // const event = JSON.parse(req.body); // res.sendStatus(200); // ответьте быстро, обработку — в очередь // }); ``` ```python import hashlib import hmac import time def verify_merchant_webhook(raw_body: bytes, headers, secret: str) -> bool: timestamp = headers.get("X-Merchant-Timestamp", "") signature = headers.get("X-Merchant-Signature", "") if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300: return False message = timestamp.encode() + b"." + raw_body expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() return hmac.compare_digest(signature, expected) ``` ### Доставка и повторы - Ответ `2xx` в течение 5 секунд — доставлено. Отвечайте сразу, а обработку ставьте в очередь. - Ошибка сети, таймаут, `5xx`, `408`, `429` — повтор через 30 с, 2 мин, 10 мин, 30 мин, 2 ч и 6 ч (всего до 7 попыток). - Редиректы (`3xx`) не выполняются, остальные `4xx` считаются окончательным отказом — без повторов. - Итог виден в поле `webhookStatus` заказа: `pending`, `delivered` или `failed`. - Одно событие может прийти больше одного раза. Обрабатывайте идемпотентно — по паре `orderId` + `status`. ### Требования к URL Только `https://`, до 2048 символов, без логина и пароля в адресе. Нельзя `localhost`, внутренние и служебные IP-адреса — ни напрямую, ни через DNS-имя, которое на них указывает. Неверный адрес — `400 Invalid webhookUrl (причина)`. В песочнице вебхук приходит только для успешной тестовой покупки, содержит `"sandbox": true` и повторяется до 3 раз. ## Ошибки Формат ошибок, коды ответов и что с ними делать. Все ответы — JSON. Успешные содержат `"status": "success"`, ошибки — `"status": "error"` и `message`; у части ошибок есть машинный `code`. ```json { "status": "error", "code": "idempotency_key_reused", "message": "This idempotencyKey was already used for a different product" } ``` ### Коды ответов | Код | Когда | Что делать | |---|---|---| | 400 | Некорректный ID, тело или параметр. | Исправить запрос. | | 401 / 403 | Нет ключа, неверный ключ, доступ отключён, покупки отключены. | См. «Аутентификация». | | 402 | Недостаточно средств. Заказ не создан. | Пополнить баланс, повторить с тем же ключом. | | 404 | Товар или заказ не найден. | Проверить ID. | | 409 | Товар недоступен для покупки. | Обновить каталог, проверить наличие. | | 413 / 415 | Тело больше 10 КБ или не JSON. | Исправить запрос. | | 422 | idempotencyKey использован для другого товара. | Новый ключ для новой покупки. | | 429 | Превышен лимит запросов. | Подождать и повторить реже. | | 500 | Внутренняя ошибка или `key_unavailable`. | Повторить с тем же idempotencyKey / запросить заказ позже. | | 502 | Поставщик не выполнил заказ (`failed`). Деньги возвращены. | Можно купить снова с новым ключом. | | 503 | Временная недоступность или техработы (`maintenance_mode`). | Повторить позже. | ### Причины отказа (failureReason) Приходят в `message` ответа покупки и в поле `failureReason` заказа со статусом `failed` или `cancelled`. | failureReason | Значение | |---|---| | `insufficient_balance` | Недостаточно средств. | | `purchase_disabled` | Покупки для клиента отключены. | | `product_unavailable` | Товар нельзя продать сейчас: нет в наличии или цена у поставщика вышла за допустимую. | | `fulfillment_failed` | Поставщик не выполнил заказ. | | `fulfillment_cancelled` | Поставщик отменил или вернул заказ. | | `client_not_found` | Клиент не найден. | Список может пополняться: неизвестное значение обрабатывайте как общий отказ. ## Лимиты и ограничения Сколько запросов можно делать и какие есть ограничения на данные. | Ограничение | Значение | |---|---| | Запросы с одного IP (все, кроме /health/api) | 600 за 15 минут | | Запросы одного клиента (по ключу) | 150 за 15 минут | | Песочница с одного IP, дополнительно | 30 за 15 минут | | Тело запроса | 10 КБ, только application/json | | X-Entity-Key · idempotencyKey · webhookUrl | 128 · 128 · 2048 символов | Превышение лимита — `429 Too many requests, try again later`. Окно фиксированное: счётчик обнуляется через 15 минут после первого запроса в окне. Значения могут меняться — не стройте логику на точных числах. ### Как не упираться в лимит - Кэшируйте каталог: `GET /products` у нас и так обновляется раз в 10 минут. - Ждите вебхук вместо частого опроса заказов; если опрашиваете — не чаще раза в 5–10 секунд. - Не проверяйте наличие всех товаров подряд — только того, который покупаете. ### Блокировки Многократные запросы к несуществующим адресам без действующего ключа воспринимаются как сканирование: IP временно блокируется (`403 IP banned`). ### Технические работы Во время работ все запросы, кроме `/health/api`, получают `503` с `"code": "maintenance_mode"` и заголовком `X-Maintenance-Mode: 1`. Повторите позже. # Справочник эндпоинтов ## Каталог ### GET /products — Список товаров Весь каталог с ценами и признаком «в продаже». Возвращает все товары каталога, включая снятые с продажи. Отфильтруйте их сами по `isAvailable`. Ответ отдаётся из кэша, который обновляется раз в 10 минут: изменение цены может появиться здесь с задержкой. Свежие данные одного товара — `GET /products/{id}/info`, живая проверка наличия перед покупкой — `GET /products/{id}/availability`. Авторизация: заголовок `X-Entity-Key`. **Ответы** - `200 OK` — Каталог ```json { "status": "success", "products": [ { "id": 12, "title": "Dead by Daylight — Steam Key", "price": 1490, "currency": "RUB", "region": "ru", "platform": "steam", "isAvailable": true, "images": { "42x42": "https://cdn.example.com/p/12-42.png", "156x156": "https://cdn.example.com/p/12-156.png", "512x512": null } }, { "id": 13, "title": "Dead by Daylight — Chapter DLC", "price": 590, "currency": "RUB", "region": "ru", "platform": "steam", "isAvailable": false, "images": { "42x42": "https://cdn.example.com/p/12-42.png", "156x156": "https://cdn.example.com/p/12-156.png", "512x512": null } } ] } ``` - `401 Unauthorized` — Нет ключа ```json { "status": "error", "message": "Missing X-Entity-Key header" } ``` - `403 Forbidden` — Неверный ключ ```json { "status": "error", "message": "Invalid entity key" } ``` - `429 Too Many Requests` — Превышен лимит ```json { "status": "error", "message": "Too many requests, try again later" } ``` ### GET /products/{id}/info — Информация о товаре Один товар: название, цена, валюта, регион, платформа, статус, изображения. Данные читаются из базы при каждом запросе, без кэша — изменения видны сразу. `isAvailable` — наш признак «в продаже». Он не гарантирует, что товар есть у поставщика прямо сейчас: для этого есть `GET /products/{id}/availability`. Авторизация: заголовок `X-Entity-Key`. **Параметры пути** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `id` | integer | да | ID товара, целое положительное число. Пример: `12`. | **Ответы** - `200 OK` — Товар ```json { "status": "success", "product": { "id": 12, "title": "Dead by Daylight — Steam Key", "price": 1490, "currency": "RUB", "region": "ru", "platform": "steam", "isAvailable": true, "images": { "42x42": "https://cdn.example.com/p/12-42.png", "156x156": "https://cdn.example.com/p/12-156.png", "512x512": null } } } ``` - `400 Bad Request` — Некорректный ID ```json { "status": "error", "message": "Invalid product id" } ``` - `404 Not Found` — Товар не найден ```json { "status": "error", "message": "Product not found" } ``` - `401 Unauthorized` — Нет ключа ```json { "status": "error", "message": "Missing X-Entity-Key header" } ``` - `403 Forbidden` — Неверный ключ ```json { "status": "error", "message": "Invalid entity key" } ``` - `429 Too Many Requests` — Превышен лимит ```json { "status": "error", "message": "Too many requests, try again later" } ``` ### GET /products/{id}/availability — Проверка наличия Живая проверка: можно ли купить товар прямо сейчас. Для товаров с автоматической выдачей проверяет наличие у поставщика и допустимость цены. Для товаров с ручной выдачей возвращает наш признак «в продаже». Вызывайте непосредственно перед покупкой. `POST /purchase/{productId}` выполняет ту же проверку, так что эндпоинт и покупка не противоречат друг другу. Авторизация: заголовок `X-Entity-Key`. **Параметры пути** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `id` | integer | да | ID товара, целое положительное число. Пример: `12`. | **Ответы** - `200 OK` — Доступен ```json { "status": "success", "productId": 12, "price": 1490, "currency": "RUB", "available": true } ``` - `200 OK` — Недоступен. `reason`: `out_of_stock` — нет у поставщика; `not_configured` — товар не настроен для автовыдачи; `temporarily_unavailable` — временно нельзя продать по текущей цене. ```json { "status": "success", "productId": 12, "price": 1490, "currency": "RUB", "available": false, "reason": "out_of_stock" } ``` - `400 Bad Request` — Некорректный ID ```json { "status": "error", "message": "Invalid product id" } ``` - `404 Not Found` — Товар не найден ```json { "status": "error", "message": "Product not found" } ``` - `503 Service Unavailable` — Проверка временно недоступна ```json { "status": "error", "message": "Availability check temporarily unavailable" } ``` - `401 Unauthorized` — Нет ключа ```json { "status": "error", "message": "Missing X-Entity-Key header" } ``` - `403 Forbidden` — Неверный ключ ```json { "status": "error", "message": "Invalid entity key" } ``` - `429 Too Many Requests` — Превышен лимит ```json { "status": "error", "message": "Too many requests, try again later" } ``` ## Покупки и заказы ### POST /purchase/{productId} — Покупка товара Создаёт заказ, списывает его стоимость с баланса и выдаёт ключ. Стоимость резервируется с баланса в момент создания заказа. Если заказ в итоге не выполнен (`failed`/`cancelled`), деньги возвращаются на баланс автоматически. Товар с ручной выдачей завершается сразу (`201`). Товар с автоматической выдачей покупается у поставщика: обычно это несколько секунд, но если не уложились в ~50 секунд, вы получите `202` со статусом `pending` — заказ продолжит выполняться. Узнать исход можно через вебхук или `GET /orders/{id}`. Всегда передавайте `idempotencyKey`. Если соединение оборвалось или пришёл таймаут — повторите запрос с тем же ключом: вы получите исход исходного заказа, а не вторую покупку. Авторизация: заголовок `X-Entity-Key`. **Списывает баланс.** **Параметры пути** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `productId` | integer | да | ID товара из каталога. Пример: `12`. | **Тело запроса (JSON, до 10 КБ)** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `idempotencyKey` | string | нет | Ваш уникальный ключ покупки: 1–128 печатных ASCII-символов без пробелов, регистр важен. Повтор с тем же ключом не создаёт второй заказ и не списывает деньги второй раз — возвращает исход первого запроса с `replayed: true`. Настоятельно рекомендуется (может стать обязательным). Пример: `order-7731`. | | `webhookUrl` | string (URL) | нет | Куда отправить уведомление об исходе заказа. Только https, до 2048 символов, без логина/пароля в URL, не localhost и не внутренние адреса. Пример: `https://shop.example.com/hooks/merchant`. | **Ответы** - `201 Created` — Куплено. `key` — массив ключей от поставщика; сам ключ в поле `serial`, `type` — `text` или изображение. У товаров с ручной выдачей поля `key` нет. ```json { "status": "success", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "completed", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": "pending", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z", "key": [ { "serial": "ABCDE-FGHIJ-KLMNO", "type": "text", "name": "Dead by Daylight" } ] } } ``` - `202 Accepted` — Выполняется. Заказ создан, деньги зарезервированы, выдача ещё идёт. Дождитесь вебхука или опрашивайте `GET /orders/{id}`. ```json { "status": "pending", "message": "Purchase is still being processed", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "pending", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": null, "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" } } ``` - `201 Created` — Повтор запроса. Тот же `idempotencyKey` — возвращается исход первого запроса, второго списания нет. Код ответа совпадает с исходом заказа. ```json { "status": "success", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "completed", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": "delivered", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" }, "replayed": true } ``` - `400 Bad Request` — Некорректный запрос. Также: `Invalid idempotencyKey (1-128 printable ASCII characters, no spaces)`, `Invalid webhookUrl (must_be_https)`, `Malformed JSON body`. ```json { "status": "error", "message": "Invalid product id" } ``` - `402 Payment Required` — Недостаточно средств. Заказ не создан, ничего не списано. После пополнения можно повторить с тем же `idempotencyKey`. ```json { "status": "error", "message": "insufficient_balance" } ``` - `403 Forbidden` — Покупки отключены ```json { "status": "error", "message": "purchase_disabled" } ``` - `404 Not Found` — Товар не найден ```json { "status": "error", "message": "Product not found" } ``` - `409 Conflict` — Товар недоступен ```json { "status": "error", "message": "Product is not available for purchase" } ``` - `415 Unsupported Media Type` — Не JSON ```json { "status": "error", "message": "Content-Type must be application/json" } ``` - `422 Unprocessable Entity` — Ключ уже использован. Этот `idempotencyKey` уже использован для другого товара. ```json { "status": "error", "code": "idempotency_key_reused", "message": "This idempotencyKey was already used for a different product" } ``` - `502 Bad Gateway` — Не удалось выдать. Поставщик не выполнил заказ. Деньги возвращены на баланс. ```json { "status": "error", "message": "fulfillment_failed", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "failed", "failureReason": "fulfillment_failed", "idempotencyKey": "order-7731", "webhookStatus": "delivered", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" } } ``` - `500 Internal Server Error` — Ключ временно недоступен. Заказ оплачен и выполнен, но ключ сейчас не удалось прочитать. Повторите `GET /orders/{id}` позже или обратитесь в поддержку — повторно покупать не нужно. ```json { "status": "error", "code": "key_unavailable", "message": "The order is paid but its key can't be retrieved right now; contact support", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "completed", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": "delivered", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" } } ``` - `503 Service Unavailable` — Проверка наличия недоступна ```json { "status": "error", "message": "Availability check temporarily unavailable" } ``` - `401 Unauthorized` — Нет ключа ```json { "status": "error", "message": "Missing X-Entity-Key header" } ``` - `403 Forbidden` — Неверный ключ ```json { "status": "error", "message": "Invalid entity key" } ``` - `429 Too Many Requests` — Превышен лимит ```json { "status": "error", "message": "Too many requests, try again later" } ``` ### GET /orders/{id} — Получить заказ Статус заказа и ключ, если заказ выполнен. Возвращает только ваши заказы: чужой и несуществующий заказ одинаково дают `404`. Ключ (`key`) приходит только здесь и в ответе `POST /purchase` — в вебхуке его нет. Авторизация: заголовок `X-Entity-Key`. **Параметры пути** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `id` | integer | да | ID заказа, целое положительное число. Пример: `12`. | **Ответы** - `200 OK` — Выполнен ```json { "status": "success", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "completed", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": "delivered", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z", "key": [ { "serial": "ABCDE-FGHIJ-KLMNO", "type": "text", "name": "Dead by Daylight" } ] } } ``` - `200 OK` — В обработке ```json { "status": "success", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "pending", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": null, "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" } } ``` - `200 OK` — Не выполнен ```json { "status": "success", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "failed", "failureReason": "product_unavailable", "idempotencyKey": "order-7731", "webhookStatus": "delivered", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" } } ``` - `400 Bad Request` — Некорректный ID ```json { "status": "error", "message": "Invalid order id" } ``` - `404 Not Found` — Не найден ```json { "status": "error", "message": "Order not found" } ``` - `500 Internal Server Error` — Ключ временно недоступен ```json { "status": "error", "code": "key_unavailable", "message": "The order is paid but its key can't be retrieved right now; contact support", "order": { "id": 1842, "productId": 12, "quantity": 1, "price": 1490, "currency": "RUB", "status": "completed", "failureReason": null, "idempotencyKey": "order-7731", "webhookStatus": "delivered", "createdAt": "2026-11-13T12:36:53.000Z", "updatedAt": "2026-11-13T12:37:10.000Z" } } ``` - `401 Unauthorized` — Нет ключа ```json { "status": "error", "message": "Missing X-Entity-Key header" } ``` - `403 Forbidden` — Неверный ключ ```json { "status": "error", "message": "Invalid entity key" } ``` - `429 Too Many Requests` — Превышен лимит ```json { "status": "error", "message": "Too many requests, try again later" } ``` ## Песочница ### GET /sandbox/products — Товары песочницы Фиксированный тестовый каталог. Песочница повторяет форму боевых ответов, но не трогает баланс, заказы и статистику. Подходит любой действующий `X-Entity-Key`. Каждый товар детерминированно воспроизводит один исход — как тестовые номера карт у платёжных систем. Авторизация: заголовок `X-Entity-Key`. **Ответы** - `200 OK` — Каталог песочницы ```json { "status": "success", "sandbox": true, "products": [ { "id": "sandbox-success", "title": "[Sandbox] Always succeeds", "price": 100, "currency": "RUB", "region": "all", "platform": "all", "isAvailable": true, "images": { "42x42": null, "156x156": null, "512x512": null } }, { "id": "sandbox-insufficient-balance", "title": "[Sandbox] Always fails: insufficient balance", "price": 100, "currency": "RUB", "region": "all", "platform": "all", "isAvailable": true, "images": { "42x42": null, "156x156": null, "512x512": null } }, { "id": "sandbox-unavailable", "title": "[Sandbox] Always unavailable", "price": 100, "currency": "RUB", "region": "all", "platform": "all", "isAvailable": false, "images": { "42x42": null, "156x156": null, "512x512": null } } ] } ``` - `401 Unauthorized` — Нет ключа ```json { "status": "error", "message": "Missing X-Entity-Key header" } ``` - `403 Forbidden` — Неверный ключ ```json { "status": "error", "message": "Invalid entity key" } ``` - `429 Too Many Requests` — Превышен лимит ```json { "status": "error", "message": "Too many requests, try again later" } ``` ### POST /sandbox/purchase/{productId} — Тестовая покупка Симуляция покупки без денег. `sandbox-success` → `201`, `sandbox-insufficient-balance` → `402`, `sandbox-unavailable` → `409`, любой другой ID → `404` с подсказкой. `idempotencyKey` и `webhookUrl` работают так же, как в боевом API. Вебхук приходит только для успешного заказа, подписан тем же секретом и содержит `"sandbox": true`; до 3 попыток. Тестовые заказы хранятся 24 часа. Лимит песочницы отдельный и строже боевого. Авторизация: заголовок `X-Entity-Key`. **Параметры пути** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `productId` | string | да | ID товара песочницы. Пример: `sandbox-success`. | **Тело запроса (JSON, до 10 КБ)** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `idempotencyKey` | string | нет | Как в боевом API, до 128 символов. Пример: `order-7731`. | | `webhookUrl` | string (URL) | нет | Куда отправить уведомление об исходе заказа. Только https, до 2048 символов, без логина/пароля в URL, не localhost и не внутренние адреса. Пример: `https://shop.example.com/hooks/merchant`. | **Ответы** - `201 Created` — Успех ```json { "status": "success", "order": { "id": "sandbox_px8cqEgyiFCA", "clientId": 7, "productId": "sandbox-success", "quantity": 1, "idempotencyKey": "sb-1", "price": 100, "currency": "RUB", "status": "completed", "failureReason": null, "webhookUrl": null, "webhookStatus": null, "webhookAttempts": 0, "sandbox": true, "createdAt": "2026-11-13T12:36:52.979Z", "updatedAt": "2026-11-13T12:36:52.980Z" } } ``` - `201 Created` — Повтор запроса ```json { "status": "success", "order": { "id": "sandbox_px8cqEgyiFCA", "clientId": 7, "productId": "sandbox-success", "quantity": 1, "idempotencyKey": "sb-1", "price": 100, "currency": "RUB", "status": "completed", "failureReason": null, "webhookUrl": null, "webhookStatus": null, "webhookAttempts": 0, "sandbox": true, "createdAt": "2026-11-13T12:36:52.979Z", "updatedAt": "2026-11-13T12:36:52.980Z" }, "replayed": true } ``` - `402 Payment Required` — Недостаточно средств ```json { "status": "error", "message": "insufficient_balance", "order": { "id": "sandbox_LO3g828mKFl_", "clientId": 7, "productId": "sandbox-insufficient-balance", "quantity": 1, "idempotencyKey": null, "price": 100, "currency": "RUB", "status": "failed", "failureReason": "insufficient_balance", "webhookUrl": null, "webhookStatus": null, "webhookAttempts": 0, "sandbox": true, "createdAt": "2026-11-13T12:36:52.979Z", "updatedAt": "2026-11-13T12:36:52.980Z" } } ``` - `409 Conflict` — Недоступен ```json { "status": "error", "message": "Product is not available for purchase" } ``` - `404 Not Found` — Неизвестный товар ```json { "status": "error", "message": "Product not found", "hint": "Sandbox product ids: sandbox-success, sandbox-insufficient-balance, sandbox-unavailable" } ``` - `429 Too Many Requests` — Лимит песочницы ```json { "status": "error", "message": "Too many sandbox requests, try again later" } ``` - `503 Service Unavailable` — Песочница недоступна ```json { "status": "error", "message": "Sandbox is temporarily unavailable" } ``` ### GET /sandbox/orders/{id} — Тестовый заказ Тестовый заказ по ID (вида sandbox_…). Видны только ваши тестовые заказы, не старше 24 часов. Авторизация: заголовок `X-Entity-Key`. **Параметры пути** | Имя | Тип | Обязателен | Описание | |---|---|---|---| | `id` | string | да | ID тестового заказа из ответа покупки. Пример: `sandbox_px8cqEgyiFCA`. | **Ответы** - `200 OK` — Заказ ```json { "status": "success", "order": { "id": "sandbox_px8cqEgyiFCA", "clientId": 7, "productId": "sandbox-success", "quantity": 1, "idempotencyKey": "sb-1", "price": 100, "currency": "RUB", "status": "completed", "failureReason": null, "webhookUrl": null, "webhookStatus": null, "webhookAttempts": 0, "sandbox": true, "createdAt": "2026-11-13T12:36:52.979Z", "updatedAt": "2026-11-13T12:36:52.980Z" } } ``` - `404 Not Found` — Не найден ```json { "status": "error", "message": "Order not found" } ``` ## Служебное ### GET /health/api — Состояние сервиса Проверка доступности для мониторинга. Без авторизации. `db` — база данных, `redis` — кэш. Без кэша сервис работает (`degraded`), без базы — нет (`503`). Авторизация: не нужна. **Ответы** - `200 OK` — Работает ```json { "status": "ok", "timestamp": 1794573411400, "db": "ok", "redis": "ok" } ``` - `200 OK` — Без кэша ```json { "status": "degraded", "timestamp": 1794573411400, "db": "ok", "redis": "unavailable" } ``` - `503 Service Unavailable` — Не работает ```json { "status": "error", "timestamp": 1794573411400, "db": "unavailable", "redis": "ok" } ```