{
  "openapi": "3.1.0",
  "info": {
    "title": "EasyDonat Partner API",
    "version": "1.0.0",
    "summary": "Каталог, покупки, заказы, баланс и вебхуки для партнёров EasyDonat.",
    "description": "API для партнёров EasyDonat: игровая валюта, подарочные карты, ключи игр, пополнение Steam, Telegram Stars и Premium.\n\n**Доступ.** Ключ создаётся в кабинете («Интеграция») и передаётся только в заголовке: `X-API-Key: ed_live_…` или `Authorization: Bearer ed_live_…`. Полный ключ показывается один раз. Доступ можно ограничить списком IP-адресов.\n\n**Ответы.** JSON в UTF-8. Успех — `{ \"ok\": true, … }`, ошибка — `{ \"ok\": false, \"error\": \"текст\", \"code\": \"код\" }`; коды — в схеме `ErrorCode`. Время — ISO 8601 UTC.\n\n**Деньги.** Доллары США. Суммы — строками с 2 знаками (`\"12.50\"`), цена единицы (звезды, единицы валюты Steam) — строкой до 9 знаков (`\"0.0176\"`). Покупка сразу списывает деньги с баланса; если заказ не выполнится, они вернутся (статус `refunded`).\n\n**Покупки.** Новый заказ — HTTP 201. Заголовок `Idempotency-Key` защищает от двойной покупки: повтор с тем же ключом и тем же телом вернёт прежний заказ (HTTP 200) без второго списания; тот же ключ с другим телом или с `max_price_usd` ниже итога прежнего заказа — 409 `idempotency_conflict`. `max_price_usd` — потолок цены: итог выше — заказ не создаётся (409 `price_changed`). Статус заказа — `GET /orders/{order_id}` или вебхук.\n\n**Лимиты.** 120 запросов в минуту на ключ; на партнёра в минуту: 30 покупок, 10 проверок ID игрока, 20 проверок получателя Telegram, 10 сохранений и проверок вебхука. Эти счётчики у API свои, отдельно от кабинета; кроме них действуют общие лимиты сервиса на проверки ID и получателя — на всех партнёров вместе — и доля партнёра в поисках получателя Telegram: 10 в минуту, вместе с кабинетом и покупками. Превышение любого лимита — 429 `rate_limited` с заголовком `Retry-After` и телом-JSON, как у остальных ошибок."
  },
  "servers": [
    {
      "url": "https://easydonat.com/api/v1",
      "description": "Рабочий сервер"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Аккаунт",
      "description": "Данные аккаунта, баланс и движение денег."
    },
    {
      "name": "Заказы",
      "description": "Все заказы партнёра — из API и из кабинета, новые сверху."
    },
    {
      "name": "Пополнение игр",
      "description": "Игровая валюта по ID игрока: номиналы, поля покупателя, проверка ID и покупка."
    },
    {
      "name": "Подарочные карты",
      "description": "Карты и ваучеры. Коды приходят в `order.delivery.codes`, когда заказ выполнен."
    },
    {
      "name": "Ключи игр",
      "description": "Ключи активации игр. Ключи приходят в `order.delivery.codes`, когда заказ выполнен."
    },
    {
      "name": "Пополнение Steam",
      "description": "Пополнение кошелька Steam по логину на любую сумму в RUB, USD, UAH или KZT."
    },
    {
      "name": "Telegram",
      "description": "Telegram Stars и Telegram Premium по username. Получатель проверяется до списания денег: не найден — 422, деньги не списаны; заказ создаётся в статусе `pending`. Поисков получателя у Telegram (мимо кеша) — не больше 10 в минуту на партнёра, вместе с кабинетом и покупками; сверх — проверка отвечает 429, покупка — 503 `service_unavailable` с `Retry-After`, деньги не списаны.\n\n**Если Telegram сейчас выдаётся вручную** (временно, без автоматической выдачи), получатель не проверяется — проверяется только вид username: заказ создаётся сразу в статусе `processing` (`telegram: null`), а `POST /telegram/recipient` отвечает `found: null`. Перед покупкой на неизвестный username такой ответ — повод проверить получателя у себя."
    },
    {
      "name": "Вебхук",
      "description": "Уведомления о смене статуса заказов на ваш адрес (`PUT /account/webhook`): `order.processing`, `order.completed` и `order.refunded` (так приходит и неудача: заказ не выполнен, деньги вернулись), а также `webhook.test`.\n\n**Подпись.** Заголовок `X-EasyDonat-Signature: t=<unix-время>,v1=<hex>`, где `v1` = HMAC-SHA256 с секретом вебхука от строки `<t>.<сырое тело запроса>`. Считайте подпись по сырому телу (до разбора JSON) и отклоняйте запрос, если `t` отличается от текущего времени больше чем на 5 минут.\n\n**Доставка.** Ответ 2xx в течение 10 секунд — доставлено. Иначе повторы через 1 мин, 5 мин, 15 мин, 1 ч, 3 ч, 6 ч, 12 ч и 24 ч (всего 9 попыток), затем доставка — `failed`. Редиректы не выполняются. Отвечайте сразу, а обработку делайте в фоне: на доставки одного партнёра за проход отводится около 5 секунд — медленный ответ откладывает остальные ваши уведомления до следующего прохода. Адрес, который не ответил вовсе (DNS, соединение, таймаут), получает одну отправку за проход, а остальным его доставкам, которые ждали отправки, попытка засчитывается без отправки — срок 9 попыток идёт от события. Аккаунту, который отключён, уведомления не отправляются; после включения придут события о пропущенных сменах.\n\n**Повторы.** Одно событие может прийти больше одного раза — отсеивайте повторы по `id`, а смены статуса обрабатывайте идемпотентно по паре (`order_id`, `status`): повтор несёт заказ на момент отправки, поэтому два события с разными `id` могут сообщить один и тот же статус. Если статус сменился дважды подряд, может прийти только последнее событие: прежнее недоставленное событие заказа отменяется, как только поставлено новое. Источник правды — `GET /orders/{order_id}`.\n\n**Адрес.** Только `https://`, порт 443 (по умолчанию) или 8443, без логина и пароля в адресе, не длиннее 2048 символов, не на частные, локальные и служебные адреса — это проверяется при сохранении и при каждой отправке."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Аккаунт"
        ],
        "summary": "Аккаунт",
        "operationId": "getMe",
        "description": "Данные аккаунта и итоги: `total_orders` — число выполненных заказов, `total_spent_usd` — их сумма.",
        "responses": {
          "200": {
            "description": "Аккаунт.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                },
                "example": {
                  "ok": true,
                  "id": "3f1c2b9e-6a4d-4c8e-9b7a-2d5e8f0a1c3b",
                  "email": "partner@example.com",
                  "name": "Game Shop",
                  "created_at": "2026-09-01T08:00:00.000Z",
                  "summary": {
                    "total_spent_usd": "152.40",
                    "total_orders": 87
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/balance": {
      "get": {
        "tags": [
          "Аккаунт"
        ],
        "summary": "Баланс",
        "operationId": "getBalance",
        "description": "Текущий баланс партнёра в долларах США.",
        "responses": {
          "200": {
            "description": "Баланс.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BalanceResponse"
                },
                "example": {
                  "ok": true,
                  "balance": "20.00",
                  "currency": "USD"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/transactions": {
      "get": {
        "tags": [
          "Аккаунт"
        ],
        "summary": "Движение денег",
        "operationId": "listTransactions",
        "description": "Пополнения баланса, оплата заказов, возвраты и корректировки, новые сверху.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница журнала.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransactionsPage"
                },
                "example": {
                  "ok": true,
                  "items": [
                    {
                      "id": 3012,
                      "type": "debit",
                      "kind": "charge",
                      "amount": "0.97",
                      "balance_before": "20.97",
                      "balance_after": "20.00",
                      "order_id": "ED-000123",
                      "note": "PUBG Mobile (Auto) — 60 UC",
                      "created_at": "2026-10-21T10:00:00.000Z"
                    },
                    {
                      "id": 3011,
                      "type": "credit",
                      "kind": "topup",
                      "amount": "20.00",
                      "balance_before": "0.97",
                      "balance_after": "20.97",
                      "order_id": null,
                      "note": "Пополнение баланса",
                      "created_at": "2026-10-20T15:30:00.000Z"
                    }
                  ],
                  "total": 2,
                  "page": 1,
                  "limit": 50
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/transactions/{id}": {
      "get": {
        "tags": [
          "Аккаунт"
        ],
        "summary": "Запись журнала",
        "operationId": "getTransaction",
        "description": "Одна запись движения денег.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TransactionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Запись журнала.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransactionResponse"
                },
                "example": {
                  "ok": true,
                  "transaction": {
                    "id": 3012,
                    "type": "debit",
                    "kind": "charge",
                    "amount": "0.97",
                    "balance_before": "20.97",
                    "balance_after": "20.00",
                    "order_id": "ED-000123",
                    "note": "PUBG Mobile (Auto) — 60 UC",
                    "created_at": "2026-10-21T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/orders": {
      "get": {
        "tags": [
          "Заказы"
        ],
        "summary": "Список заказов",
        "operationId": "listOrders",
        "description": "Заказы партнёра — из API и из кабинета, новые сверху. Можно отобрать по статусу.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StatusFilter"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница заказов.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrdersPage"
                },
                "example": {
                  "ok": true,
                  "items": [
                    {
                      "order_id": "ED-000123",
                      "status": "completed",
                      "kind": "topup",
                      "product_id": "topup:pubg_mobile_auto",
                      "item_id": "60_uc",
                      "product_name": "PUBG Mobile (Auto)",
                      "item_name": "60 UC",
                      "quantity": 1,
                      "amount": null,
                      "currency": null,
                      "fields": [
                        {
                          "key": "player_id",
                          "label": "Player ID",
                          "value": "5123456789"
                        }
                      ],
                      "price_usd": "0.97",
                      "total_usd": "0.97",
                      "delivery": {
                        "codes": [],
                        "text": null
                      },
                      "failure_reason": null,
                      "telegram": null,
                      "idempotency_key": "my-order-42",
                      "channel": "api",
                      "created_at": "2026-10-21T10:00:00.000Z",
                      "updated_at": "2026-10-21T10:00:05.000Z",
                      "completed_at": "2026-10-21T10:00:05.000Z"
                    },
                    {
                      "order_id": "ED-000122",
                      "status": "completed",
                      "kind": "giftcard",
                      "product_id": "giftcard:acash_my",
                      "item_id": "10_myr",
                      "product_name": "A-Cash",
                      "item_name": "10 MYR",
                      "quantity": 2,
                      "amount": null,
                      "currency": null,
                      "fields": [],
                      "price_usd": "2.86",
                      "total_usd": "5.72",
                      "delivery": {
                        "codes": [
                          {
                            "code": "AC10-7K2M-9QXP-4LDT",
                            "pin": null,
                            "serial": null,
                            "note": null
                          },
                          {
                            "code": "AC10-3H8W-6RZN-1VBE",
                            "pin": null,
                            "serial": null,
                            "note": null
                          }
                        ],
                        "text": null
                      },
                      "failure_reason": null,
                      "telegram": null,
                      "idempotency_key": null,
                      "channel": "cabinet",
                      "created_at": "2026-10-20T18:12:00.000Z",
                      "updated_at": "2026-10-20T18:12:09.000Z",
                      "completed_at": "2026-10-20T18:12:09.000Z"
                    }
                  ],
                  "total": 2,
                  "page": 1,
                  "limit": 50
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/orders/{order_id}": {
      "get": {
        "tags": [
          "Заказы"
        ],
        "summary": "Заказ",
        "operationId": "getOrder",
        "description": "Один заказ партнёра — источник правды о статусе: после вебхука или обрыва связи проверяйте заказ здесь. Чужой или несуществующий заказ — 404.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrderIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Заказ.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "topup": {
                    "$ref": "#/components/examples/TopupCompletedResponse"
                  },
                  "premium": {
                    "$ref": "#/components/examples/PremiumCompletedResponse"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/topups": {
      "get": {
        "tags": [
          "Пополнение игр"
        ],
        "summary": "Игры",
        "operationId": "listTopups",
        "description": "Игры с пополнением по ID игрока. Список постраничный: пока `meta.has_more` — запрашивайте дальше с `cursor` = `meta.next_cursor`. Номиналы и поля покупателя — `GET /topups/offers`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CursorLimit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка игр.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopupsList"
                },
                "example": {
                  "ok": true,
                  "kind": "topup",
                  "items": [
                    {
                      "category_id": "pubg_mobile_auto",
                      "name": "PUBG Mobile (Auto)",
                      "region": "GLOBAL",
                      "note": "Region: Global\nAutomatic PUBG Mobile top-up.",
                      "imageurl": "https://easydonat.com/media/4e1f0c9a7b2d6e3f8a51.webp",
                      "can_validate": true
                    }
                  ],
                  "meta": {
                    "next_cursor": "eyJpIjoyMDB9",
                    "has_more": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/topups/offers": {
      "get": {
        "tags": [
          "Пополнение игр"
        ],
        "summary": "Номиналы и поля игры",
        "operationId": "getTopupOffers",
        "description": "Номиналы игры с ценами и поля, которые нужно спросить у покупателя (ID игрока, зона, сервер…). Ключи полей — ключи объекта `fields` в покупке.",
        "parameters": [
          {
            "$ref": "#/components/parameters/TopupCategoryId"
          }
        ],
        "responses": {
          "200": {
            "description": "Номиналы и поля.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopupOffersResponse"
                },
                "example": {
                  "ok": true,
                  "category_id": "pubg_mobile_auto",
                  "name": "PUBG Mobile (Auto)",
                  "note": "Region: Global\nAutomatic PUBG Mobile top-up.",
                  "offers": [
                    {
                      "offer_id": "60_uc",
                      "name": "60 UC",
                      "price_usd": "0.97",
                      "available": true
                    },
                    {
                      "offer_id": "325_uc",
                      "name": "325 UC",
                      "price_usd": "4.83",
                      "available": true
                    },
                    {
                      "offer_id": "660_uc",
                      "name": "660 UC",
                      "price_usd": "9.66",
                      "available": true
                    }
                  ],
                  "fields": [
                    {
                      "key": "player_id",
                      "label": "Player ID",
                      "type": "text"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/topups/validate-id": {
      "get": {
        "tags": [
          "Пополнение игр"
        ],
        "summary": "Где работает проверка ID",
        "operationId": "listValidateIdGames",
        "description": "Игры, для которых работает проверка ID игрока, и ключи полей, которые она проверяет.",
        "responses": {
          "200": {
            "description": "Список игр.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateIdGamesResponse"
                },
                "example": {
                  "ok": true,
                  "items": [
                    {
                      "category_id": "pubg_mobile_auto",
                      "fields": [
                        "player_id"
                      ]
                    },
                    {
                      "category_id": "pubg_mobile_fast",
                      "fields": [
                        "player_id"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "post": {
        "tags": [
          "Пополнение игр"
        ],
        "summary": "Проверить ID игрока",
        "operationId": "validateId",
        "description": "Проверка ID игрока до покупки — без списания денег. `valid: null` — проверить сейчас не удалось; это не значит, что ID неверный. Лимит — 10 проверок в минуту.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidateIdRequest"
              },
              "example": {
                "category_id": "pubg_mobile_auto",
                "fields": {
                  "player_id": "5123456789"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Итог проверки.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateIdResponse"
                },
                "examples": {
                  "valid": {
                    "summary": "Игрок найден",
                    "value": {
                      "ok": true,
                      "category_id": "pubg_mobile_auto",
                      "valid": true,
                      "player_name": "ProGamer777"
                    }
                  },
                  "invalid": {
                    "summary": "Такого игрока нет",
                    "value": {
                      "ok": true,
                      "category_id": "pubg_mobile_auto",
                      "valid": false,
                      "player_name": null
                    }
                  },
                  "unknown": {
                    "summary": "Проверить не удалось",
                    "value": {
                      "ok": true,
                      "category_id": "pubg_mobile_auto",
                      "valid": null,
                      "player_name": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`invalid_fields` — поле не заполнено или неверно; `product_unavailable` — для этой игры проверки нет или игра не продаётся.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_fields": {
                    "$ref": "#/components/examples/ErrorInvalidPlayerId"
                  },
                  "product_unavailable": {
                    "summary": "Проверки нет",
                    "value": {
                      "ok": false,
                      "error": "Для этой игры проверки ID нет",
                      "code": "product_unavailable"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/topups/order": {
      "post": {
        "tags": [
          "Пополнение игр"
        ],
        "summary": "Купить пополнение",
        "operationId": "createTopupOrder",
        "description": "Покупка номинала игры. Деньги списываются сразу, заказ выполняется в фоне: следите за статусом через `GET /orders/{order_id}` или вебхук.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopupOrderRequest"
              },
              "example": {
                "category_id": "pubg_mobile_auto",
                "offer_id": "60_uc",
                "fields": {
                  "player_id": "5123456789"
                },
                "max_price_usd": "1.00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Заказ создан, деньги списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "pubg": {
                    "$ref": "#/components/examples/TopupCreatedResponse"
                  }
                }
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/OrderRepeated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PurchaseConflict"
          },
          "422": {
            "description": "`invalid_fields` — поле покупателя не заполнено или неверно; `product_unavailable` — игра или номинал сейчас не продаётся. Деньги не списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_fields": {
                    "$ref": "#/components/examples/ErrorInvalidPlayerId"
                  },
                  "product_unavailable": {
                    "$ref": "#/components/examples/ErrorItemUnavailable"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/giftcards": {
      "get": {
        "tags": [
          "Подарочные карты"
        ],
        "summary": "Подарочные карты",
        "operationId": "listGiftcards",
        "description": "Виды подарочных карт. Список постраничный: пока `meta.has_more` — запрашивайте дальше с `cursor` = `meta.next_cursor`. Номиналы — `GET /giftcards/cards`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CursorLimit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка карт.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GiftcardsList"
                },
                "example": {
                  "ok": true,
                  "kind": "giftcard",
                  "items": [
                    {
                      "category_id": "acash_my",
                      "name": "A-Cash",
                      "region": "MY",
                      "note": "Region: MY\nA-Cash Malaysia gift cards.\nCodes can be safely stored and redeemed later.",
                      "imageurl": null
                    }
                  ],
                  "meta": {
                    "next_cursor": null,
                    "has_more": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/giftcards/cards": {
      "get": {
        "tags": [
          "Подарочные карты"
        ],
        "summary": "Номиналы карты",
        "operationId": "getGiftcardCards",
        "description": "Номиналы карты с ценами, остатком и пределами количества в одном заказе.",
        "parameters": [
          {
            "$ref": "#/components/parameters/GiftcardCategoryId"
          }
        ],
        "responses": {
          "200": {
            "description": "Номиналы.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GiftcardCardsResponse"
                },
                "example": {
                  "ok": true,
                  "category_id": "acash_my",
                  "name": "A-Cash",
                  "note": "Region: MY\nA-Cash Malaysia gift cards.\nCodes can be safely stored and redeemed later.",
                  "offers": [
                    {
                      "card_id": "10_myr",
                      "name": "10 MYR",
                      "price_usd": "2.86",
                      "stock": 100,
                      "min_order_quantity": 1,
                      "max_order_quantity": 100,
                      "available": true
                    },
                    {
                      "card_id": "20_myr",
                      "name": "20 MYR",
                      "price_usd": "5.71",
                      "stock": 100,
                      "min_order_quantity": 1,
                      "max_order_quantity": 100,
                      "available": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/giftcards/order": {
      "post": {
        "tags": [
          "Подарочные карты"
        ],
        "summary": "Купить карты",
        "operationId": "createGiftcardOrder",
        "description": "Покупка одной или нескольких карт одного номинала. Деньги списываются сразу; коды — в `order.delivery.codes`, когда заказ `completed`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GiftcardOrderRequest"
              },
              "example": {
                "category_id": "acash_my",
                "card_id": "10_myr",
                "quantity": 2,
                "max_price_usd": "6.00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Заказ создан, деньги списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "acash": {
                    "$ref": "#/components/examples/GiftcardCreatedResponse"
                  }
                }
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/OrderRepeated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PurchaseConflict"
          },
          "422": {
            "description": "`product_unavailable` — карта или номинал сейчас не продаётся; `out_of_stock` — столько нет в наличии; `quantity_out_of_range` — количество вне пределов номинала (`min`, `max`). Деньги не списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "product_unavailable": {
                    "$ref": "#/components/examples/ErrorItemUnavailable"
                  },
                  "out_of_stock": {
                    "$ref": "#/components/examples/ErrorOutOfStock"
                  },
                  "quantity_out_of_range": {
                    "$ref": "#/components/examples/ErrorQuantityRange"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/gamekeys": {
      "get": {
        "tags": [
          "Ключи игр"
        ],
        "summary": "Игры с ключами",
        "operationId": "listGamekeys",
        "description": "Игры с ключами — в том числе те, чьи издания сейчас не продаются: можно ли купить издание, видно по `available` в `GET /gamekeys/keys`. Список постраничный: пока `meta.has_more` — запрашивайте дальше с `cursor` = `meta.next_cursor`. Издания и цены — `GET /gamekeys/keys`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/CursorLimit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка игр.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GamekeysList"
                },
                "example": {
                  "ok": true,
                  "kind": "gamekey",
                  "items": [
                    {
                      "game_id": "7_days_to_die_global",
                      "name": "7 Days to Die",
                      "region": "GLOBAL",
                      "platform": "Steam",
                      "imageurl": "https://easydonat.com/media/9b0d2e7c4a1f6e3d5c88.webp"
                    }
                  ],
                  "meta": {
                    "next_cursor": null,
                    "has_more": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/gamekeys/keys": {
      "get": {
        "tags": [
          "Ключи игр"
        ],
        "summary": "Ключи игры",
        "operationId": "getGamekeyKeys",
        "description": "Издания игры с ценами, остатком и пределами количества в одном заказе.",
        "parameters": [
          {
            "$ref": "#/components/parameters/GameId"
          }
        ],
        "responses": {
          "200": {
            "description": "Ключи игры.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GamekeyKeysResponse"
                },
                "example": {
                  "ok": true,
                  "game_id": "7_days_to_die_global",
                  "name": "7 Days to Die",
                  "region": "GLOBAL",
                  "platform": "Steam",
                  "keys": [
                    {
                      "key_id": "7_days_to_die",
                      "name": "7 Days to Die",
                      "price_usd": "18.07",
                      "stock": 121,
                      "min_order_quantity": 1,
                      "max_order_quantity": 100,
                      "available": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/gamekeys/order": {
      "post": {
        "tags": [
          "Ключи игр"
        ],
        "summary": "Купить ключи",
        "operationId": "createGamekeyOrder",
        "description": "Покупка одного или нескольких ключей одного издания. Деньги списываются сразу; ключи — в `order.delivery.codes`, когда заказ `completed`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GamekeyOrderRequest"
              },
              "example": {
                "game_id": "7_days_to_die_global",
                "key_id": "7_days_to_die",
                "quantity": 1,
                "max_price_usd": "18.50"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Заказ создан, деньги списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "key": {
                    "$ref": "#/components/examples/GamekeyCreatedResponse"
                  }
                }
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/OrderRepeated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/PurchaseConflict"
          },
          "422": {
            "description": "`product_unavailable` — игра или издание сейчас не продаётся; `out_of_stock` — столько нет в наличии; `quantity_out_of_range` — количество вне пределов (`min`, `max`). Деньги не списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "product_unavailable": {
                    "$ref": "#/components/examples/ErrorItemUnavailable"
                  },
                  "out_of_stock": {
                    "$ref": "#/components/examples/ErrorOutOfStock"
                  },
                  "quantity_out_of_range": {
                    "$ref": "#/components/examples/ErrorQuantityRange"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/steam-topup/rates": {
      "get": {
        "tags": [
          "Пополнение Steam"
        ],
        "summary": "Цены пополнения Steam",
        "operationId": "getSteamTopupRates",
        "description": "Валюты кошелька Steam, цена 1 единицы валюты в долларах и пределы суммы. Итог заказа считает сервер: примерно `amount × usd_per_unit`, вверх до цента (не выше этой оценки).",
        "responses": {
          "200": {
            "description": "Валюты и цены.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SteamRatesResponse"
                },
                "example": {
                  "ok": true,
                  "base": "USD",
                  "currencies": [
                    {
                      "currency": "RUB",
                      "usd_per_unit": "0.013159444",
                      "min_amount": 100,
                      "max_amount": 100000
                    },
                    {
                      "currency": "USD",
                      "usd_per_unit": "1.1",
                      "min_amount": 1,
                      "max_amount": 1000
                    },
                    {
                      "currency": "UAH",
                      "usd_per_unit": "0.024471189",
                      "min_amount": 50,
                      "max_amount": 50000
                    },
                    {
                      "currency": "KZT",
                      "usd_per_unit": "0.002425579",
                      "min_amount": 500,
                      "max_amount": 500000
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/steam-topup/order": {
      "post": {
        "tags": [
          "Пополнение Steam"
        ],
        "summary": "Пополнить Steam",
        "operationId": "createSteamTopupOrder",
        "description": "Пополнение кошелька Steam по логину на сумму в выбранной валюте. Деньги списываются сразу, итог — в `order.total_usd`. Если Steam не примет логин, заказ не выполнится и деньги вернутся на баланс.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SteamTopupOrderRequest"
              },
              "example": {
                "steamLogin": "example_login",
                "currency": "RUB",
                "amount": 500,
                "max_price_usd": "7.00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Заказ создан, деньги списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "rub500": {
                    "$ref": "#/components/examples/SteamCreatedResponse"
                  }
                }
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/OrderRepeated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/PurchaseConflict"
          },
          "422": {
            "description": "`invalid_fields` — логин не указан или неверен (`field`: `steamLogin`); `quantity_out_of_range` — сумма вне пределов валюты (`min`, `max`); `product_unavailable` — пополнение в этой валюте или пополнение Steam вообще сейчас недоступно (тогда и `GET /steam-topup/rates` отдаёт пустой `currencies`). Деньги не списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_fields": {
                    "summary": "Нет логина",
                    "value": {
                      "ok": false,
                      "error": "Укажите «Логин Steam»",
                      "code": "invalid_fields",
                      "field": "steamLogin"
                    }
                  },
                  "quantity_out_of_range": {
                    "summary": "Сумма меньше минимальной",
                    "value": {
                      "ok": false,
                      "error": "Минимальная сумма — 100 RUB",
                      "code": "quantity_out_of_range",
                      "min": 100,
                      "max": 100000
                    }
                  },
                  "product_unavailable": {
                    "summary": "Валюта недоступна",
                    "value": {
                      "ok": false,
                      "error": "Пополнение в этой валюте сейчас недоступно",
                      "code": "product_unavailable"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/telegram/stars": {
      "get": {
        "tags": [
          "Telegram"
        ],
        "summary": "Цена звёзд",
        "operationId": "getTelegramStars",
        "description": "Цена одной звезды и сколько звёзд можно купить одним заказом. Итог заказа — `quantity × price_per_star`, вверх до цента.",
        "responses": {
          "200": {
            "description": "Цена и пределы.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StarsQuoteResponse"
                },
                "example": {
                  "ok": true,
                  "kind": "telegram_stars",
                  "price_per_star": "0.0176",
                  "min_amount": 50,
                  "max_amount": 11363
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/telegram/premium": {
      "get": {
        "tags": [
          "Telegram"
        ],
        "summary": "Цены Premium",
        "operationId": "getTelegramPremium",
        "description": "Сроки Telegram Premium и их цены.",
        "responses": {
          "200": {
            "description": "Сроки и цены.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PremiumQuoteResponse"
                },
                "example": {
                  "ok": true,
                  "kind": "telegram_premium",
                  "plans": [
                    {
                      "months": 3,
                      "price_usd": "12.66",
                      "available": true
                    },
                    {
                      "months": 6,
                      "price_usd": "17.31",
                      "available": true
                    },
                    {
                      "months": 12,
                      "price_usd": "31.25",
                      "available": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/telegram/recipient": {
      "post": {
        "tags": [
          "Telegram"
        ],
        "summary": "Проверить получателя",
        "operationId": "checkTelegramRecipient",
        "description": "Проверка получателя до покупки — без списания денег: есть ли такой аккаунт и может ли он получить звёзды или Premium. `found: null` — проверить сейчас нельзя (в том числе пока Telegram выдаётся вручную). Лимит — 20 проверок в минуту; из них поисков у Telegram мимо кеша — не больше 10 в минуту на партнёра, вместе с кабинетом и покупками (сверх — 429).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TelegramRecipientRequest"
              },
              "example": {
                "telegram_username": "@example_user",
                "product": "premium",
                "months": 3
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Итог проверки.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipientResponse"
                },
                "examples": {
                  "found": {
                    "summary": "Получатель найден",
                    "value": {
                      "ok": true,
                      "found": true,
                      "name": "Example User",
                      "message": null
                    }
                  },
                  "not_found": {
                    "summary": "Аккаунта нет",
                    "value": {
                      "ok": true,
                      "found": false,
                      "name": null,
                      "message": "Аккаунт @example_user в Telegram не найден"
                    }
                  },
                  "already_premium": {
                    "summary": "Premium уже есть",
                    "value": {
                      "ok": true,
                      "found": false,
                      "name": null,
                      "message": "У @example_user уже есть Telegram Premium — подарить нельзя"
                    }
                  },
                  "unavailable": {
                    "summary": "Проверить нельзя",
                    "value": {
                      "ok": true,
                      "found": null,
                      "name": null,
                      "message": "Проверка сейчас недоступна — попробуйте через минуту"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "description": "`invalid_fields` — username неверного вида (`field`: `telegram_username`); `product_unavailable` — товар сейчас не продаётся (звёзды сняты с продажи, срок Premium недоступен).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_fields": {
                    "$ref": "#/components/examples/ErrorInvalidUsername"
                  },
                  "product_unavailable": {
                    "$ref": "#/components/examples/ErrorStarsUnavailable"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/telegram/stars/buy": {
      "post": {
        "tags": [
          "Telegram"
        ],
        "summary": "Купить звёзды",
        "operationId": "buyTelegramStars",
        "description": "Покупка Telegram Stars для username. Получатель проверяется до списания денег. Выдача идёт в фоне: этап — в `order.telegram.stage`, итог — статус заказа.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StarsBuyRequest"
              },
              "example": {
                "telegram_username": "@example_user",
                "quantity": 500,
                "max_price_usd": "9.00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Заказ создан, деньги списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "stars500": {
                    "$ref": "#/components/examples/StarsCreatedResponse"
                  }
                }
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/OrderRepeated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/PurchaseConflict"
          },
          "422": {
            "description": "`invalid_fields` — username неверного вида (`field`: `telegram_username`); `quantity_out_of_range` — число звёзд вне пределов (`min`, `max` — те же, что `min_amount` и `max_amount` в `GET /telegram/stars`: максимум уже с потолком суммы одного заказа); `order_limit` — даже минимум звёзд дороже потолка одного заказа; `recipient_not_found` — аккаунт не найден; `not_a_user` — username принадлежит каналу или боту; `product_unavailable` — звёзды сейчас не продаются. Деньги не списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_fields": {
                    "$ref": "#/components/examples/ErrorInvalidUsername"
                  },
                  "quantity_out_of_range": {
                    "summary": "Меньше минимума",
                    "value": {
                      "ok": false,
                      "error": "Минимум — 50 звёзд",
                      "code": "quantity_out_of_range",
                      "min": 50,
                      "max": 11363
                    }
                  },
                  "order_limit": {
                    "$ref": "#/components/examples/ErrorOrderLimit"
                  },
                  "recipient_not_found": {
                    "$ref": "#/components/examples/ErrorRecipientNotFound"
                  },
                  "not_a_user": {
                    "$ref": "#/components/examples/ErrorNotAUser"
                  },
                  "product_unavailable": {
                    "$ref": "#/components/examples/ErrorStarsUnavailable"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/telegram/premium/buy": {
      "post": {
        "tags": [
          "Telegram"
        ],
        "summary": "Подарить Premium",
        "operationId": "buyTelegramPremium",
        "description": "Подарок Telegram Premium на 3, 6 или 12 месяцев для username. Получатель проверяется до списания денег. Выдача идёт в фоне: этап — в `order.telegram.stage`, итог — статус заказа.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PremiumBuyRequest"
              },
              "example": {
                "telegram_username": "example_user",
                "months": 3,
                "max_price_usd": "12.66"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Заказ создан, деньги списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                },
                "examples": {
                  "premium3": {
                    "$ref": "#/components/examples/PremiumCreatedResponse"
                  }
                }
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/OrderRepeated"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientFunds"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/PurchaseConflict"
          },
          "422": {
            "description": "`invalid_fields` — username неверного вида (`field`: `telegram_username`); `recipient_not_found` — аккаунт не найден; `already_premium` — у получателя уже есть Premium; `not_a_user` — username принадлежит каналу или боту; `order_limit` — сумма больше потолка одного заказа; `product_unavailable` — этот срок сейчас не продаётся. Деньги не списаны.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_fields": {
                    "$ref": "#/components/examples/ErrorInvalidUsername"
                  },
                  "recipient_not_found": {
                    "$ref": "#/components/examples/ErrorRecipientNotFound"
                  },
                  "already_premium": {
                    "summary": "Premium уже есть",
                    "value": {
                      "ok": false,
                      "error": "У @example_user уже есть Telegram Premium — подарить нельзя",
                      "code": "already_premium"
                    }
                  },
                  "not_a_user": {
                    "$ref": "#/components/examples/ErrorNotAUser"
                  },
                  "order_limit": {
                    "$ref": "#/components/examples/ErrorOrderLimit"
                  },
                  "product_unavailable": {
                    "$ref": "#/components/examples/ErrorItemUnavailable"
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/account/webhook": {
      "get": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Настройки вебхука",
        "operationId": "getWebhook",
        "description": "Адрес, секрет подписи и состояние доставок. Не настроен — `webhook: null`.",
        "responses": {
          "200": {
            "description": "Вебхук или null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                },
                "example": {
                  "ok": true,
                  "webhook": {
                    "url": "https://shop.example.com/hooks/easydonat",
                    "enabled": true,
                    "secret": "whsec_0123456789abcdef0123456789abcdef",
                    "consecutive_failures": 0,
                    "last_success_at": "2026-10-21T10:00:05.400Z",
                    "last_failure_at": null,
                    "last_failure_reason": null,
                    "created_at": "2026-10-20T09:00:00.000Z",
                    "updated_at": "2026-10-20T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "put": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Сохранить вебхук",
        "operationId": "putWebhook",
        "description": "Создать вебхук или сменить адрес и включение. При первом сохранении создаётся секрет подписи `whsec_…`. Уведомления приходят о каждой следующей смене статуса — в том числе у заказов, созданных раньше и ещё не завершённых; о прошлых сменах уведомлений не будет. Без `enabled`: у нового вебхука — включён, у существующего — прежнее значение. Адрес проверяется (в том числе по DNS) при каждом сохранении.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPutRequest"
              },
              "example": {
                "url": "https://shop.example.com/hooks/easydonat",
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Вебхук сохранён.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                },
                "example": {
                  "ok": true,
                  "webhook": {
                    "url": "https://shop.example.com/hooks/easydonat",
                    "enabled": true,
                    "secret": "whsec_0123456789abcdef0123456789abcdef",
                    "consecutive_failures": 0,
                    "last_success_at": null,
                    "last_failure_at": null,
                    "last_failure_reason": null,
                    "created_at": "2026-10-20T09:00:00.000Z",
                    "updated_at": "2026-10-20T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — тело не той формы или адрес не подходит (не `https://`, порт не 443 и не 8443, логин и пароль в адресе, длиннее 2048 символов — считая уже закодированные символы не латиницей, частный или локальный адрес).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "error": "Адрес вебхука должен начинаться с https://",
                  "code": "invalid_request"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Удалить вебхук",
        "operationId": "deleteWebhook",
        "description": "Удаляет вебхук и останавливает отправку уведомлений.",
        "responses": {
          "200": {
            "description": "Вебхук удалён.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/account/webhook/secret/regenerate": {
      "post": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Новый секрет",
        "operationId": "regenerateWebhookSecret",
        "description": "Создаёт новый секрет подписи. Старый перестаёт действовать сразу — обновите его у себя: уведомления, которые ещё не ушли, будут подписаны новым. Тело запроса не нужно: пустое тело принимается при любом `Content-Type`, `{}` — тоже; другое тело — 400.",
        "responses": {
          "200": {
            "description": "Новый секрет.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                },
                "example": {
                  "ok": true,
                  "secret": "whsec_fedcba9876543210fedcba9876543210"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/EmptyBodyExpected"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/WebhookNotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/account/webhook/test": {
      "post": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Проверить вебхук",
        "operationId": "testWebhook",
        "description": "Одна отправка события `webhook.test` на ваш адрес, без повторов. Итог отправки — всегда HTTP 200: `ok` — ответил ли ваш адрес кодом 2xx. Тело запроса не нужно: пустое тело принимается при любом `Content-Type`, `{}` — тоже; другое тело — 400.",
        "responses": {
          "200": {
            "description": "Итог отправки.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                },
                "examples": {
                  "delivered": {
                    "summary": "Доставлено",
                    "value": {
                      "ok": true,
                      "status": 200,
                      "error": null
                    }
                  },
                  "failed": {
                    "summary": "Адрес не ответил",
                    "value": {
                      "ok": false,
                      "status": null,
                      "error": "Нет ответа за 10 секунд"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/EmptyBodyExpected"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/WebhookNotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/account/webhook/deliveries": {
      "get": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Журнал доставок",
        "operationId": "listWebhookDeliveries",
        "description": "Последние 50 доставок уведомлений, новые сверху.",
        "responses": {
          "200": {
            "description": "Доставки.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveriesResponse"
                },
                "example": {
                  "ok": true,
                  "items": [
                    {
                      "id": 1043,
                      "event": "order.completed",
                      "event_id": "evt_3b1f6c2a9d8e4f70",
                      "order_id": "ED-000123",
                      "attempt": 1,
                      "status": "delivered",
                      "response_status": 200,
                      "duration_ms": 184,
                      "error": null,
                      "created_at": "2026-10-21T10:00:05.000Z",
                      "next_retry_at": null
                    },
                    {
                      "id": 1042,
                      "event": "order.processing",
                      "event_id": "evt_8a4c1e7f2b9d6035",
                      "order_id": "ED-000124",
                      "attempt": 2,
                      "status": "pending",
                      "response_status": 500,
                      "duration_ms": 92,
                      "error": "Ответ HTTP 500",
                      "created_at": "2026-10-21T10:05:03.000Z",
                      "next_retry_at": "2026-10-21T10:11:03.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "order.processing": {
      "post": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Заказ выполняется",
        "operationId": "onOrderProcessing",
        "description": "Заказ начал выполняться. Приходит при смене статуса любого заказа партнёра — из API и из кабинета. Подпись, повторы и сроки ответа — в описании раздела «Вебхук».",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryHeader"
          },
          {
            "$ref": "#/components/parameters/SignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "steam": {
                  "$ref": "#/components/examples/EventOrderProcessing"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Принято. Ответьте в течение 10 секунд."
          },
          "default": {
            "description": "Не принято — повтор по расписанию."
          }
        }
      }
    },
    "order.completed": {
      "post": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Заказ выполнен",
        "operationId": "onOrderCompleted",
        "description": "Заказ выполнен; коды карт и ключи — в `data.order.delivery`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryHeader"
          },
          {
            "$ref": "#/components/parameters/SignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "pubg": {
                  "$ref": "#/components/examples/EventOrderCompleted"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Принято. Ответьте в течение 10 секунд."
          },
          "default": {
            "description": "Не принято — повтор по расписанию."
          }
        }
      }
    },
    "order.refunded": {
      "post": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Деньги вернулись",
        "operationId": "onOrderRefunded",
        "description": "Деньги за заказ вернулись на баланс партнёра: заказ не выполнен или отменён, причина — в `data.order.failure_reason`. Это и есть уведомление о неудаче: отдельного события о статусе `failed` нет — неудача сразу становится возвратом.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryHeader"
          },
          {
            "$ref": "#/components/parameters/SignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "steam": {
                  "$ref": "#/components/examples/EventOrderRefunded"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Принято. Ответьте в течение 10 секунд."
          },
          "default": {
            "description": "Не принято — повтор по расписанию."
          }
        }
      }
    },
    "webhook.test": {
      "post": {
        "tags": [
          "Вебхук"
        ],
        "summary": "Проверка вебхука",
        "operationId": "onWebhookTest",
        "description": "Отправляется по `POST /account/webhook/test` один раз, без повторов. Подписана так же, как остальные события.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryHeader"
          },
          {
            "$ref": "#/components/parameters/SignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "test": {
                  "$ref": "#/components/examples/EventWebhookTest"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Принято. Ответьте в течение 10 секунд."
          },
          "default": {
            "description": "Не принято."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Ключ API из кабинета («Интеграция»): `ed_live_` и 32 шестнадцатеричных символа. Только в заголовке, никогда в адресе запроса."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Тот же ключ в заголовке `Authorization: Bearer ed_live_…`. Если присланы оба заголовка с разными ключами — 401."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Защита от двойной покупки (очень рекомендуем): 8–128 символов — латинские буквы, цифры, `-` и `_`; один ключ — один заказ. Повтор с тем же ключом и тем же телом — HTTP 200 и прежний заказ без второго списания; тот же ключ с другим телом — 409 `idempotency_conflict`. Тело сравнивается по смыслу: товар, номинал, количество, сумма и значения полей покупателя после той же обработки, что у покупки (пробелы по краям, `@` и `t.me/` у username); поля, которых у товара нет, не учитываются (их отбрасывает и первая покупка). `max_price_usd` повтора ниже итога прежнего заказа — тоже 409 `idempotency_conflict`; выше или равен (или его нет) — прежний заказ. Без ключа каждый запрос — новый заказ. Ключи общие для API и кабинета партнёра.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{8,128}$"
        },
        "example": "my-order-42"
      },
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "description": "Номер страницы, с 1.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Записей на странице.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "StatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "Только заказы в этом статусе; не указан — все.",
        "schema": {
          "$ref": "#/components/schemas/OrderStatus"
        }
      },
      "CursorLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Записей на странице.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 500,
          "default": 200
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Продолжение списка — `meta.next_cursor` из прошлого ответа.",
        "schema": {
          "type": "string"
        }
      },
      "TopupCategoryId": {
        "name": "category_id",
        "in": "query",
        "required": true,
        "description": "`category_id` игры из `GET /topups`.",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "example": "pubg_mobile_auto"
      },
      "GiftcardCategoryId": {
        "name": "category_id",
        "in": "query",
        "required": true,
        "description": "`category_id` карты из `GET /giftcards`.",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "example": "acash_my"
      },
      "GameId": {
        "name": "game_id",
        "in": "query",
        "required": true,
        "description": "`game_id` игры из `GET /gamekeys`.",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "example": "7_days_to_die_global"
      },
      "TransactionId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "id записи журнала.",
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "example": 3012
      },
      "OrderIdPath": {
        "name": "order_id",
        "in": "path",
        "required": true,
        "description": "Номер заказа, например `ED-000123` (регистр не важен).",
        "schema": {
          "type": "string",
          "pattern": "^[Ee][Dd]-[0-9]{1,15}$"
        },
        "example": "ED-000123"
      },
      "EventHeader": {
        "name": "X-EasyDonat-Event",
        "in": "header",
        "required": true,
        "description": "Тип события — то же, что `type` в теле.",
        "schema": {
          "$ref": "#/components/schemas/WebhookEventType"
        },
        "example": "order.completed"
      },
      "DeliveryHeader": {
        "name": "X-EasyDonat-Delivery",
        "in": "header",
        "required": true,
        "description": "id доставки — как `id` в журнале доставок (`GET /account/webhook/deliveries`). У всех попыток одного события он один и тот же (номер попытки — `attempt` в журнале); повторы отсеивайте по `id` события в теле.",
        "schema": {
          "type": "string"
        },
        "example": "1043"
      },
      "SignatureHeader": {
        "name": "X-EasyDonat-Signature",
        "in": "header",
        "required": true,
        "description": "`t=<unix-время>,v1=<hex>`: `v1` = HMAC-SHA256 от строки `<t>.<сырое тело запроса>`; ключ HMAC — секрет вебхука целиком, вместе с `whsec_`, в UTF-8. Каждая попытка подписывается заново, со свежим `t`. Отклоняйте запрос, если подпись не совпала или `t` отличается от текущего времени больше чем на 5 минут.",
        "schema": {
          "type": "string",
          "pattern": "^t=[0-9]+,v1=[0-9a-f]{64}$"
        },
        "example": "t=1792576805,v1=6f1e0c2b9a8d7e3f5c4b1a0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c"
      }
    },
    "headers": {
      "RetryAfter": {
        "description": "Через сколько секунд повторить запрос.",
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "example": 30
      }
    },
    "responses": {
      "OrderRepeated": {
        "description": "Повтор с тем же `Idempotency-Key` и тем же телом — прежний заказ в текущем состоянии, без второго списания.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OrderResponse"
            }
          }
        }
      },
      "BadRequest": {
        "description": "`invalid_request` — тело не JSON или не той формы, неизвестное поле, неверный параметр или заголовок `Idempotency-Key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Некорректные данные заказа",
              "code": "invalid_request"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`unauthorized` — нет ключа, ключ неверный или отозван.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Нужен действующий ключ API",
              "code": "unauthorized"
            }
          }
        }
      },
      "InsufficientFunds": {
        "description": "`insufficient_funds` — не хватает баланса: `need_usd` — сколько не хватает, `balance_usd` — текущий баланс. Деньги не списаны.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Недостаточно средств на балансе",
              "code": "insufficient_funds",
              "need_usd": "3.20",
              "balance_usd": "5.60"
            }
          }
        }
      },
      "Forbidden": {
        "description": "`ip_not_allowed` — запрос не с разрешённого IP-адреса; `account_disabled` — аккаунт отключён.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "ip_not_allowed": {
                "summary": "Адрес не в списке",
                "value": {
                  "ok": false,
                  "error": "Запрос с адреса, которого нет в списке разрешённых",
                  "code": "ip_not_allowed"
                }
              },
              "account_disabled": {
                "summary": "Аккаунт отключён",
                "value": {
                  "ok": false,
                  "error": "Аккаунт отключён",
                  "code": "account_disabled"
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found` — нет такого товара (по id из каталога), номинала, заказа или записи журнала.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "product": {
                "summary": "Товар",
                "value": {
                  "ok": false,
                  "error": "Товар не найден или снят с продажи",
                  "code": "not_found"
                }
              },
              "order": {
                "summary": "Заказ",
                "value": {
                  "ok": false,
                  "error": "Заказ не найден",
                  "code": "not_found"
                }
              }
            }
          }
        }
      },
      "WebhookNotFound": {
        "description": "`not_found` — вебхук не настроен.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Вебхук не настроен",
              "code": "not_found"
            }
          }
        }
      },
      "EmptyBodyExpected": {
        "description": "`invalid_request` — тело не пустое и не `{}`: у этого вызова параметров нет. Не JSON — «Ожидается JSON (Content-Type: application/json)», JSON с полями (или не объект) — «Тело запроса не нужно: пустое или {}».",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Тело запроса не нужно: пустое или {}",
              "code": "invalid_request"
            }
          }
        }
      },
      "PurchaseConflict": {
        "description": "`price_changed` — итог выше `max_price_usd`, в `total_usd` — текущий итог; `idempotency_conflict` — этот `Idempotency-Key` уже использован для другого заказа (другие товар, номинал, количество, сумма или поля) или итог того заказа выше `max_price_usd` повтора. Деньги не списаны.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "price_changed": {
                "summary": "Цена выше потолка",
                "value": {
                  "ok": false,
                  "error": "Цена изменилась: было $0.95, стало $0.97",
                  "code": "price_changed",
                  "total_usd": "0.97"
                }
              },
              "idempotency_conflict": {
                "summary": "Ключ уже использован",
                "value": {
                  "ok": false,
                  "error": "Этот ключ запроса уже использован для другого заказа",
                  "code": "idempotency_conflict"
                }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "`rate_limited` — превышен лимит запросов; повторите через `Retry-After` секунд.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Слишком много покупок подряд. Попробуйте через 12 секунд.",
              "code": "rate_limited"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "`service_unavailable` — покупки временно выключены, каталог загружается, выдача товара на паузе или получателя Telegram сейчас не проверить (в том числе исчерпан лимит поисков); повторите через `Retry-After` секунд. Деньги не списаны.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "paused": {
                "summary": "Товар временно не продаётся",
                "value": {
                  "ok": false,
                  "error": "Покупка этого товара временно недоступна",
                  "code": "service_unavailable"
                }
              },
              "catalog": {
                "summary": "Каталог загружается",
                "value": {
                  "ok": false,
                  "error": "Каталог ещё загружается, попробуйте через минуту",
                  "code": "service_unavailable"
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "`internal` — сбой у нас. Покупку можно повторить с тем же `Idempotency-Key`: второго списания не будет.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "Внутренняя ошибка сервиса",
              "code": "internal"
            }
          }
        }
      }
    },
    "schemas": {
      "Ok": {
        "type": "boolean",
        "const": true
      },
      "MoneyUsd": {
        "type": "string",
        "pattern": "^[0-9]+\\.[0-9]{2}$",
        "description": "Доллары США строкой с 2 знаками после точки.",
        "examples": [
          "12.50"
        ]
      },
      "BalanceUsd": {
        "type": "string",
        "pattern": "^-?[0-9]+\\.[0-9]{2}$",
        "description": "Баланс в долларах США строкой с 2 знаками после точки.",
        "examples": [
          "20.00"
        ]
      },
      "UnitPriceUsd": {
        "type": "string",
        "pattern": "^[0-9]+(\\.[0-9]{1,9})?$",
        "description": "Цена единицы в долларах США строкой, до 9 знаков после точки.",
        "examples": [
          "0.0176"
        ]
      },
      "MaxPriceUsd": {
        "description": "Потолок итога заказа в долларах — строка или число. Итог выше — 409 `price_changed`, деньги не списаны. Сравнивается с точностью до цента.",
        "anyOf": [
          {
            "type": "string",
            "pattern": "^[0-9]+(\\.[0-9]+)?$"
          },
          {
            "type": "number",
            "minimum": 0
          }
        ],
        "examples": [
          "1.00"
        ]
      },
      "Timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "Время в ISO 8601 UTC.",
        "examples": [
          "2026-10-21T10:00:00.000Z"
        ]
      },
      "OrderId": {
        "type": "string",
        "pattern": "^ED-[0-9]{1,15}$",
        "description": "Номер заказа EasyDonat.",
        "examples": [
          "ED-000123"
        ]
      },
      "ImageUrl": {
        "type": [
          "string",
          "null"
        ],
        "format": "uri",
        "description": "Картинка товара (WebP 128×128) на easydonat.com или null.",
        "examples": [
          "https://easydonat.com/media/4e1f0c9a7b2d6e3f8a51.webp"
        ]
      },
      "Region": {
        "type": [
          "string",
          "null"
        ],
        "description": "Регион товара, если известен: `GLOBAL`, `BR`, `CIS`, `MY`…",
        "examples": [
          "GLOBAL"
        ]
      },
      "CatalogNote": {
        "type": [
          "string",
          "null"
        ],
        "description": "Описание товара (часто на английском) или null.",
        "examples": [
          "Region: Global\nAutomatic PUBG Mobile top-up."
        ]
      },
      "CursorMeta": {
        "type": "object",
        "required": [
          "next_cursor",
          "has_more"
        ],
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Передайте в `cursor` за следующей страницей; null — это всё."
          },
          "has_more": {
            "type": "boolean",
            "description": "Есть ли ещё страницы."
          }
        }
      },
      "FieldOption": {
        "type": "object",
        "required": [
          "value",
          "label"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "Что передать в `fields`."
          },
          "label": {
            "type": "string",
            "description": "Что показать покупателю."
          }
        }
      },
      "BuyerField": {
        "type": "object",
        "description": "Поле, которое нужно спросить у покупателя.",
        "required": [
          "key",
          "label",
          "type"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Ключ в объекте `fields` тела покупки.",
            "examples": [
              "player_id"
            ]
          },
          "label": {
            "type": "string",
            "description": "Подпись поля.",
            "examples": [
              "Player ID"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "select"
            ],
            "description": "`text` — ввод строки; `select` — выбор из `options`."
          },
          "options": {
            "type": "array",
            "description": "Только у `select`: передавайте `value` одного из вариантов.",
            "items": {
              "$ref": "#/components/schemas/FieldOption"
            }
          }
        }
      },
      "OrderStatus": {
        "type": "string",
        "enum": [
          "pending",
          "processing",
          "completed",
          "failed",
          "refunded"
        ],
        "description": "`pending` — принят, деньги списаны, выполнение ещё не началось; `processing` — выполняется; `completed` — выполнен; `failed` — не выполнен, деньги вот-вот вернутся (обычно неудача сразу становится возвратом, и этот статус вы не увидите); `refunded` — не выполнен или отменён, деньги вернулись на баланс. Окончательные статусы — `completed` и `refunded`; о неудаче сообщает `order.refunded`."
      },
      "OrderKind": {
        "type": "string",
        "enum": [
          "topup",
          "giftcard",
          "gamekey",
          "steam_topup",
          "telegram",
          "manual"
        ],
        "description": "`topup` — пополнение игр; `giftcard` — подарочная карта; `gamekey` — ключ игры; `steam_topup` — пополнение Steam; `telegram` — Telegram Stars и Premium; `manual` — услуга, купленная в кабинете (через API v1 не продаётся)."
      },
      "OrderChannel": {
        "type": "string",
        "enum": [
          "api",
          "cabinet"
        ],
        "description": "Где создан заказ: через API или в кабинете."
      },
      "OrderField": {
        "type": "object",
        "required": [
          "key",
          "label",
          "value"
        ],
        "properties": {
          "key": {
            "type": "string",
            "examples": [
              "player_id"
            ]
          },
          "label": {
            "type": "string",
            "examples": [
              "Player ID"
            ]
          },
          "value": {
            "type": "string",
            "description": "Что указал покупатель; у секретных полей — `••••`.",
            "examples": [
              "5123456789"
            ]
          }
        }
      },
      "DeliveryCode": {
        "type": "object",
        "description": "Код подарочной карты или ключ игры.",
        "required": [
          "code",
          "pin",
          "serial",
          "note"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Код или ключ.",
            "examples": [
              "AC10-7K2M-9QXP-4LDT"
            ]
          },
          "pin": {
            "type": [
              "string",
              "null"
            ],
            "description": "PIN, если есть."
          },
          "serial": {
            "type": [
              "string",
              "null"
            ],
            "description": "Серийный номер, если есть."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Пояснение к коду, если есть."
          }
        }
      },
      "OrderDelivery": {
        "type": "object",
        "description": "Что выдано по заказу.",
        "required": [
          "codes",
          "text"
        ],
        "properties": {
          "codes": {
            "type": "array",
            "description": "Коды карт и ключи игр; у остальных товаров — пустой список.",
            "items": {
              "$ref": "#/components/schemas/DeliveryCode"
            }
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "description": "Выдача текстом (например, у Telegram) или null.",
            "examples": [
              "Отправлено 500 звёзд на @example_user"
            ]
          }
        }
      },
      "TelegramStage": {
        "type": "string",
        "enum": [
          "queued",
          "preparing",
          "paying",
          "delivered",
          "checking"
        ],
        "description": "`queued` — в очереди; `preparing` — готовим отправку; `paying` — отправляем; `delivered` — выдано; `checking` — заказ проверяется вручную."
      },
      "TelegramProgress": {
        "type": "object",
        "description": "Ход выдачи Telegram Stars и Premium.",
        "required": [
          "stage",
          "test"
        ],
        "properties": {
          "stage": {
            "$ref": "#/components/schemas/TelegramStage"
          },
          "test": {
            "type": "boolean",
            "description": "Тестовый заказ: звёзды и Premium на самом деле не отправляются, деньги возвращаются на баланс."
          }
        }
      },
      "Order": {
        "type": "object",
        "description": "Заказ партнёра.",
        "required": [
          "order_id",
          "status",
          "kind",
          "product_id",
          "item_id",
          "product_name",
          "item_name",
          "quantity",
          "amount",
          "currency",
          "fields",
          "price_usd",
          "total_usd",
          "delivery",
          "failure_reason",
          "telegram",
          "idempotency_key",
          "channel",
          "created_at",
          "updated_at",
          "completed_at"
        ],
        "properties": {
          "order_id": {
            "$ref": "#/components/schemas/OrderId"
          },
          "status": {
            "$ref": "#/components/schemas/OrderStatus"
          },
          "kind": {
            "$ref": "#/components/schemas/OrderKind"
          },
          "product_id": {
            "type": "string",
            "description": "id товара: `<kind>:<category_id или game_id>`; у Steam — `steam_topup:wallet`, у Telegram — `telegram:stars` и `telegram:premium`.",
            "examples": [
              "topup:pubg_mobile_auto"
            ]
          },
          "item_id": {
            "type": "string",
            "description": "Номинал: `offer_id`, `card_id` или `key_id`; у Steam — валюта (`RUB`…); у звёзд — `stars`; у Premium — срок в месяцах (`3`, `6`, `12`).",
            "examples": [
              "60_uc"
            ]
          },
          "product_name": {
            "type": "string",
            "examples": [
              "PUBG Mobile (Auto)"
            ]
          },
          "item_name": {
            "type": "string",
            "examples": [
              "60 UC"
            ]
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Количество; у пополнений игр, Steam и Telegram — 1."
          },
          "amount": {
            "type": [
              "number",
              "null"
            ],
            "description": "У Steam — сумма в валюте `currency`; у звёзд Telegram — число звёзд; иначе null."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "RUB",
              "USD",
              "UAH",
              "KZT",
              null
            ],
            "description": "У Steam — валюта кошелька; иначе null."
          },
          "fields": {
            "type": "array",
            "description": "Что указал покупатель.",
            "items": {
              "$ref": "#/components/schemas/OrderField"
            }
          },
          "price_usd": {
            "$ref": "#/components/schemas/MoneyUsd",
            "description": "Цена за единицу."
          },
          "total_usd": {
            "$ref": "#/components/schemas/MoneyUsd",
            "description": "Итог — столько списано с баланса."
          },
          "delivery": {
            "description": "Выдача — только у `completed`, иначе null. У пополнений игр и Steam товар зачисляется прямо на аккаунт, кодов нет.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/OrderDelivery"
              },
              {
                "type": "null"
              }
            ]
          },
          "failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Почему заказ не выполнен — по-русски; иначе null.",
            "examples": [
              "Steam не принял данные: проверьте логин и сумму"
            ]
          },
          "telegram": {
            "description": "Ход выдачи Telegram Stars и Premium; у остальных — null.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/TelegramProgress"
              },
              {
                "type": "null"
              }
            ]
          },
          "idempotency_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "`Idempotency-Key`, с которым создан заказ; без ключа и у заказов из кабинета — null."
          },
          "channel": {
            "$ref": "#/components/schemas/OrderChannel"
          },
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "updated_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Когда выполнен; иначе null."
          }
        }
      },
      "Transaction": {
        "type": "object",
        "description": "Запись движения денег.",
        "required": [
          "id",
          "type",
          "kind",
          "amount",
          "balance_before",
          "balance_after",
          "order_id",
          "note",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "type": {
            "type": "string",
            "enum": [
              "credit",
              "debit"
            ],
            "description": "`credit` — зачисление, `debit` — списание."
          },
          "kind": {
            "type": "string",
            "enum": [
              "topup",
              "charge",
              "refund",
              "adjust"
            ],
            "description": "`topup` — пополнение баланса; `charge` — оплата заказа; `refund` — возврат за заказ; `adjust` — корректировка (например, возврат за тестовый заказ Telegram)."
          },
          "amount": {
            "$ref": "#/components/schemas/MoneyUsd",
            "description": "Сумма, всегда положительная; направление — в `type`."
          },
          "balance_before": {
            "$ref": "#/components/schemas/BalanceUsd",
            "description": "Баланс до записи."
          },
          "balance_after": {
            "$ref": "#/components/schemas/BalanceUsd",
            "description": "Баланс после записи."
          },
          "order_id": {
            "description": "Заказ, к которому относится запись, или null.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/OrderId"
              },
              {
                "type": "null"
              }
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Пояснение по-русски.",
            "examples": [
              "PUBG Mobile (Auto) — 60 UC"
            ]
          },
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "Webhook": {
        "type": "object",
        "description": "Настройки вебхука.",
        "required": [
          "url",
          "enabled",
          "secret",
          "consecutive_failures",
          "last_success_at",
          "last_failure_at",
          "last_failure_reason",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://shop.example.com/hooks/easydonat"
            ]
          },
          "enabled": {
            "type": "boolean",
            "description": "Отправляются ли уведомления."
          },
          "secret": {
            "type": "string",
            "pattern": "^whsec_[0-9a-f]{32}$",
            "description": "Секрет подписи уведомлений. Храните его так же бережно, как ключ API."
          },
          "consecutive_failures": {
            "type": "integer",
            "minimum": 0,
            "description": "Неудачных попыток подряд: считается каждая попытка, в том числе повторы одного события и проверки `webhook.test`; удачная попытка обнуляет счётчик, смена адреса — тоже."
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_failure_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Почему не удалась последняя неудачная попытка."
          },
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "updated_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "Delivery": {
        "type": "object",
        "description": "Доставка уведомления на адрес вебхука.",
        "required": [
          "id",
          "event",
          "event_id",
          "order_id",
          "attempt",
          "status",
          "response_status",
          "duration_ms",
          "error",
          "created_at",
          "next_retry_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1,
            "description": "id доставки — как в заголовке `X-EasyDonat-Delivery`."
          },
          "event": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]+$",
            "description": "id события — как `id` в теле уведомления."
          },
          "order_id": {
            "description": "Заказ события; у `webhook.test` — null.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/OrderId"
              },
              {
                "type": "null"
              }
            ]
          },
          "attempt": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9,
            "description": "Номер попытки, 1–9. Если ваш адрес не ответил вовсе (DNS, соединение, таймаут), попытка засчитывается и остальным доставкам, которые ждали отправки, — без отправки (в `error` так и написано): срок 9 попыток идёт от события."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed"
            ],
            "description": "`pending` — ждёт отправки или повтора; `delivered` — ваш адрес ответил 2xx; `failed` — все попытки неудачны, либо доставка отменена (по заказу поставлено событие о новом статусе, вебхук удалён) или проверка прервалась — причина в `error`."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP-статус ответа; null — ответа не было."
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Сколько длилась отправка, мс."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Что пошло не так."
          },
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "next_retry_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Когда следующий повтор; null — повторов больше не будет."
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": [
          "order.processing",
          "order.completed",
          "order.refunded",
          "webhook.test"
        ],
        "description": "`order.processing`, `order.completed`, `order.refunded` — заказ перешёл в этот статус (`order.refunded` — заказ не выполнен и деньги вернулись: отдельного события о неудаче нет); `webhook.test` — проверка вебхука."
      },
      "WebhookEvent": {
        "type": "object",
        "description": "Тело уведомления.",
        "required": [
          "id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]+$",
            "description": "id события. Одно событие может прийти больше одного раза — отсеивайте повторы по нему, а сами смены статуса обрабатывайте идемпотентно по паре (`order_id`, `status`)."
          },
          "type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "data": {
            "type": "object",
            "description": "У событий `order.*` — заказ на момент отправки: в повторе `type` остаётся исходным, а `data.order.status` — текущий (заказ мог успеть выполниться). Статус берите из `data.order.status` и обрабатывайте идемпотентно по паре (`order_id`, `status`): два события с разными `id` могут нести один и тот же статус — второе пропускайте. Как только по заказу поставлено событие о новом статусе, прежнее недоставленное отменяется. У `webhook.test` поля `order` может не быть.",
            "properties": {
              "order": {
                "$ref": "#/components/schemas/Order"
              }
            }
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "invalid_request",
          "unauthorized",
          "insufficient_funds",
          "ip_not_allowed",
          "account_disabled",
          "not_found",
          "price_changed",
          "idempotency_conflict",
          "invalid_fields",
          "product_unavailable",
          "out_of_stock",
          "quantity_out_of_range",
          "order_limit",
          "recipient_not_found",
          "already_premium",
          "not_a_user",
          "rate_limited",
          "service_unavailable",
          "internal"
        ],
        "description": "| HTTP | code | Когда |\n|---|---|---|\n| 400 | `invalid_request` | тело не JSON или не той формы, неизвестное поле, неверный параметр |\n| 401 | `unauthorized` | нет ключа, ключ неверный или отозван |\n| 402 | `insufficient_funds` | не хватает баланса; доп. поля `need_usd` (сколько не хватает), `balance_usd` |\n| 403 | `ip_not_allowed` | запрос не с разрешённого IP-адреса |\n| 403 | `account_disabled` | аккаунт отключён |\n| 404 | `not_found` | нет такого товара (по id из каталога), номинала, заказа, записи журнала |\n| 409 | `price_changed` | итог выше `max_price_usd`; доп. поле `total_usd` — текущий итог. Деньги не списаны |\n| 409 | `idempotency_conflict` | этот `Idempotency-Key` уже использован для другого заказа или итог того заказа выше `max_price_usd` повтора |\n| 422 | `invalid_fields` | поле покупателя не заполнено или неверно; доп. поле `field` |\n| 422 | `product_unavailable` | товар или номинал сейчас не продаётся (у Steam, звёзд и Premium — и когда товара сейчас нет в каталоге) |\n| 422 | `out_of_stock` | столько нет в наличии |\n| 422 | `quantity_out_of_range` | количество или сумма вне пределов; доп. поля `min`, `max` |\n| 422 | `order_limit` | сумма заказа больше потолка одного заказа |\n| 422 | `recipient_not_found` | аккаунт Telegram не найден |\n| 422 | `already_premium` | у получателя уже есть Telegram Premium |\n| 422 | `not_a_user` | username принадлежит не пользователю (канал, бот) |\n| 429 | `rate_limited` | лимит запросов; заголовок `Retry-After` |\n| 503 | `service_unavailable` | покупки временно выключены, каталог загружается, выдача на паузе, получателя Telegram сейчас не проверить; `Retry-After` |\n| 500 | `internal` | сбой у нас; покупку можно повторить с тем же `Idempotency-Key` |"
      },
      "Error": {
        "type": "object",
        "description": "Ошибка. Ветвитесь по `code`, текст `error` — для людей.",
        "required": [
          "ok",
          "error",
          "code"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "description": "Причина по-русски.",
            "examples": [
              "Недостаточно средств на балансе"
            ]
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "need_usd": {
            "$ref": "#/components/schemas/MoneyUsd",
            "description": "Только у `insufficient_funds`: сколько не хватает."
          },
          "balance_usd": {
            "$ref": "#/components/schemas/BalanceUsd",
            "description": "Только у `insufficient_funds`: текущий баланс."
          },
          "total_usd": {
            "$ref": "#/components/schemas/MoneyUsd",
            "description": "Только у `price_changed`: текущий итог заказа."
          },
          "field": {
            "type": "string",
            "description": "Только у `invalid_fields`: какое поле — ключ из `fields`, `steamLogin` или `telegram_username`.",
            "examples": [
              "player_id"
            ]
          },
          "min": {
            "type": "number",
            "description": "Только у `quantity_out_of_range`: наименьшее допустимое количество или сумма."
          },
          "max": {
            "type": "number",
            "description": "Только у `quantity_out_of_range`: наибольшее допустимое количество или сумма."
          }
        }
      },
      "TopupCategory": {
        "type": "object",
        "description": "Игра с пополнением.",
        "required": [
          "category_id",
          "name",
          "region",
          "note",
          "imageurl",
          "can_validate"
        ],
        "properties": {
          "category_id": {
            "type": "string",
            "examples": [
              "pubg_mobile_auto"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "PUBG Mobile (Auto)"
            ]
          },
          "region": {
            "$ref": "#/components/schemas/Region"
          },
          "note": {
            "$ref": "#/components/schemas/CatalogNote"
          },
          "imageurl": {
            "$ref": "#/components/schemas/ImageUrl"
          },
          "can_validate": {
            "type": "boolean",
            "description": "Работает проверка ID игрока (`POST /topups/validate-id`)."
          }
        }
      },
      "TopupOffer": {
        "type": "object",
        "description": "Номинал пополнения.",
        "required": [
          "offer_id",
          "name",
          "price_usd",
          "available"
        ],
        "properties": {
          "offer_id": {
            "type": "string",
            "examples": [
              "60_uc"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "60 UC"
            ]
          },
          "price_usd": {
            "$ref": "#/components/schemas/MoneyUsd"
          },
          "available": {
            "type": "boolean",
            "description": "Можно ли купить сейчас."
          }
        }
      },
      "GiftcardCategory": {
        "type": "object",
        "description": "Подарочная карта.",
        "required": [
          "category_id",
          "name",
          "region",
          "note",
          "imageurl"
        ],
        "properties": {
          "category_id": {
            "type": "string",
            "examples": [
              "acash_my"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "A-Cash"
            ]
          },
          "region": {
            "$ref": "#/components/schemas/Region"
          },
          "note": {
            "$ref": "#/components/schemas/CatalogNote"
          },
          "imageurl": {
            "$ref": "#/components/schemas/ImageUrl"
          }
        }
      },
      "GiftcardOffer": {
        "type": "object",
        "description": "Номинал подарочной карты.",
        "required": [
          "card_id",
          "name",
          "price_usd",
          "stock",
          "min_order_quantity",
          "max_order_quantity",
          "available"
        ],
        "properties": {
          "card_id": {
            "type": "string",
            "examples": [
              "10_myr"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "10 MYR"
            ]
          },
          "price_usd": {
            "$ref": "#/components/schemas/MoneyUsd"
          },
          "stock": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Остаток; null — не сообщается."
          },
          "min_order_quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Минимум штук в одном заказе."
          },
          "max_order_quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Максимум штук в одном заказе (не больше 100 и не больше остатка)."
          },
          "available": {
            "type": "boolean",
            "description": "Можно ли купить сейчас."
          }
        }
      },
      "GamekeyGame": {
        "type": "object",
        "description": "Игра с ключами.",
        "required": [
          "game_id",
          "name",
          "region",
          "platform",
          "imageurl"
        ],
        "properties": {
          "game_id": {
            "type": "string",
            "examples": [
              "7_days_to_die_global"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "7 Days to Die"
            ]
          },
          "region": {
            "$ref": "#/components/schemas/Region"
          },
          "platform": {
            "type": [
              "string",
              "null"
            ],
            "description": "Где активируется ключ: `Steam`, `Xbox`, `Rockstar`…",
            "examples": [
              "Steam"
            ]
          },
          "imageurl": {
            "$ref": "#/components/schemas/ImageUrl"
          }
        }
      },
      "GamekeyOffer": {
        "type": "object",
        "description": "Издание игры.",
        "required": [
          "key_id",
          "name",
          "price_usd",
          "stock",
          "min_order_quantity",
          "max_order_quantity",
          "available"
        ],
        "properties": {
          "key_id": {
            "type": "string",
            "examples": [
              "7_days_to_die"
            ]
          },
          "name": {
            "type": "string",
            "examples": [
              "7 Days to Die"
            ]
          },
          "price_usd": {
            "$ref": "#/components/schemas/MoneyUsd"
          },
          "stock": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Остаток; null — не сообщается."
          },
          "min_order_quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Минимум штук в одном заказе."
          },
          "max_order_quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "Максимум штук в одном заказе (не больше 100 и не больше остатка)."
          },
          "available": {
            "type": "boolean",
            "description": "Можно ли купить сейчас."
          }
        }
      },
      "SteamCurrency": {
        "type": "string",
        "enum": [
          "RUB",
          "USD",
          "UAH",
          "KZT"
        ],
        "description": "Валюта кошелька Steam."
      },
      "SteamCurrencyRate": {
        "type": "object",
        "description": "Цена пополнения Steam в одной валюте.",
        "required": [
          "currency",
          "usd_per_unit",
          "min_amount",
          "max_amount"
        ],
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/SteamCurrency"
          },
          "usd_per_unit": {
            "$ref": "#/components/schemas/UnitPriceUsd",
            "description": "Сколько долларов спишется за 1 единицу валюты."
          },
          "min_amount": {
            "type": "number",
            "description": "Наименьшая сумма пополнения в этой валюте.",
            "examples": [
              100
            ]
          },
          "max_amount": {
            "type": "number",
            "description": "Наибольшая сумма пополнения в этой валюте.",
            "examples": [
              100000
            ]
          }
        }
      },
      "PremiumMonths": {
        "type": "integer",
        "enum": [
          3,
          6,
          12
        ],
        "description": "Срок Telegram Premium в месяцах."
      },
      "PremiumPlan": {
        "type": "object",
        "required": [
          "months",
          "price_usd",
          "available"
        ],
        "properties": {
          "months": {
            "$ref": "#/components/schemas/PremiumMonths"
          },
          "price_usd": {
            "$ref": "#/components/schemas/MoneyUsd"
          },
          "available": {
            "type": "boolean",
            "description": "Можно ли купить сейчас."
          }
        }
      },
      "TelegramUsername": {
        "type": "string",
        "minLength": 1,
        "description": "Username получателя: с `@` или без, можно ссылкой `t.me/…`; 5–32 символа — латинские буквы, цифры и `_`.",
        "examples": [
          "@example_user"
        ]
      },
      "BuyerFieldValues": {
        "type": "object",
        "description": "Поля покупателя: ключ — `fields[].key` из `GET /topups/offers`, значение — строка до 128 символов; у `select` — `value` одного из вариантов. Каждое поле обязательно, лишние ключи не учитываются.",
        "additionalProperties": {
          "type": "string"
        },
        "examples": [
          {
            "player_id": "5123456789"
          }
        ]
      },
      "TopupOrderRequest": {
        "type": "object",
        "required": [
          "category_id",
          "offer_id",
          "fields"
        ],
        "additionalProperties": false,
        "properties": {
          "category_id": {
            "type": "string",
            "minLength": 1,
            "description": "`category_id` из `GET /topups`."
          },
          "offer_id": {
            "type": "string",
            "minLength": 1,
            "description": "`offer_id` из `GET /topups/offers` этой игры."
          },
          "fields": {
            "$ref": "#/components/schemas/BuyerFieldValues"
          },
          "max_price_usd": {
            "$ref": "#/components/schemas/MaxPriceUsd"
          }
        }
      },
      "ValidateIdRequest": {
        "type": "object",
        "required": [
          "category_id",
          "fields"
        ],
        "additionalProperties": false,
        "properties": {
          "category_id": {
            "type": "string",
            "minLength": 1,
            "description": "`category_id` игры с `can_validate: true`."
          },
          "fields": {
            "$ref": "#/components/schemas/BuyerFieldValues"
          }
        }
      },
      "GiftcardOrderRequest": {
        "type": "object",
        "required": [
          "category_id",
          "card_id",
          "quantity"
        ],
        "additionalProperties": false,
        "properties": {
          "category_id": {
            "type": "string",
            "minLength": 1,
            "description": "`category_id` из `GET /giftcards`."
          },
          "card_id": {
            "type": "string",
            "minLength": 1,
            "description": "`card_id` из `GET /giftcards/cards` этой карты."
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Сколько карт; в пределах `min_order_quantity`…`max_order_quantity` и остатка."
          },
          "max_price_usd": {
            "$ref": "#/components/schemas/MaxPriceUsd"
          }
        }
      },
      "GamekeyOrderRequest": {
        "type": "object",
        "required": [
          "game_id",
          "key_id",
          "quantity"
        ],
        "additionalProperties": false,
        "properties": {
          "game_id": {
            "type": "string",
            "minLength": 1,
            "description": "`game_id` из `GET /gamekeys`."
          },
          "key_id": {
            "type": "string",
            "minLength": 1,
            "description": "`key_id` из `GET /gamekeys/keys` этой игры."
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Сколько ключей; в пределах `min_order_quantity`…`max_order_quantity` и остатка."
          },
          "max_price_usd": {
            "$ref": "#/components/schemas/MaxPriceUsd"
          }
        }
      },
      "SteamTopupOrderRequest": {
        "type": "object",
        "required": [
          "steamLogin",
          "currency",
          "amount"
        ],
        "additionalProperties": false,
        "properties": {
          "steamLogin": {
            "type": "string",
            "minLength": 1,
            "description": "Логин аккаунта Steam (не имя профиля), до 128 символов.",
            "examples": [
              "example_login"
            ]
          },
          "currency": {
            "$ref": "#/components/schemas/SteamCurrency"
          },
          "amount": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Сумма пополнения в валюте `currency`, не больше 2 знаков после точки; в пределах `min_amount`…`max_amount` из `GET /steam-topup/rates`.",
            "examples": [
              500
            ]
          },
          "max_price_usd": {
            "$ref": "#/components/schemas/MaxPriceUsd"
          }
        }
      },
      "TelegramRecipientRequest": {
        "type": "object",
        "required": [
          "telegram_username",
          "product"
        ],
        "additionalProperties": false,
        "properties": {
          "telegram_username": {
            "$ref": "#/components/schemas/TelegramUsername"
          },
          "product": {
            "type": "string",
            "enum": [
              "stars",
              "premium"
            ],
            "description": "Что собираетесь купить."
          },
          "months": {
            "$ref": "#/components/schemas/PremiumMonths",
            "description": "Только для `premium`: срок."
          }
        }
      },
      "StarsBuyRequest": {
        "type": "object",
        "required": [
          "telegram_username",
          "quantity"
        ],
        "additionalProperties": false,
        "properties": {
          "telegram_username": {
            "$ref": "#/components/schemas/TelegramUsername"
          },
          "quantity": {
            "type": "integer",
            "minimum": 50,
            "description": "Число звёзд; в пределах `min_amount`…`max_amount` из `GET /telegram/stars`.",
            "examples": [
              500
            ]
          },
          "max_price_usd": {
            "$ref": "#/components/schemas/MaxPriceUsd"
          }
        }
      },
      "PremiumBuyRequest": {
        "type": "object",
        "required": [
          "telegram_username",
          "months"
        ],
        "additionalProperties": false,
        "properties": {
          "telegram_username": {
            "$ref": "#/components/schemas/TelegramUsername"
          },
          "months": {
            "$ref": "#/components/schemas/PremiumMonths"
          },
          "max_price_usd": {
            "$ref": "#/components/schemas/MaxPriceUsd"
          }
        }
      },
      "WebhookPutRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "description": "Только `https://`, порт 443 (по умолчанию) или 8443, без логина и пароля в адресе, не длиннее 2048 символов (после кодирования: не-ASCII в пути считается как `%XX`), не на частные, локальные и служебные адреса.",
            "examples": [
              "https://shop.example.com/hooks/easydonat"
            ]
          },
          "enabled": {
            "type": "boolean",
            "description": "Включить или выключить отправку уведомлений."
          }
        }
      },
      "OkResponse": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          }
        }
      },
      "MeResponse": {
        "type": "object",
        "required": [
          "ok",
          "id",
          "email",
          "name",
          "created_at",
          "summary"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "id": {
            "type": "string",
            "description": "id партнёра."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "summary": {
            "type": "object",
            "required": [
              "total_spent_usd",
              "total_orders"
            ],
            "properties": {
              "total_spent_usd": {
                "$ref": "#/components/schemas/MoneyUsd",
                "description": "Сумма выполненных заказов."
              },
              "total_orders": {
                "type": "integer",
                "minimum": 0,
                "description": "Число выполненных заказов."
              }
            }
          }
        }
      },
      "BalanceResponse": {
        "type": "object",
        "required": [
          "ok",
          "balance",
          "currency"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "balance": {
            "$ref": "#/components/schemas/BalanceUsd"
          },
          "currency": {
            "type": "string",
            "const": "USD"
          }
        }
      },
      "TransactionsPage": {
        "type": "object",
        "required": [
          "ok",
          "items",
          "total",
          "page",
          "limit"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Transaction"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Всего записей."
          },
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      },
      "TransactionResponse": {
        "type": "object",
        "required": [
          "ok",
          "transaction"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "transaction": {
            "$ref": "#/components/schemas/Transaction"
          }
        }
      },
      "OrdersPage": {
        "type": "object",
        "required": [
          "ok",
          "items",
          "total",
          "page",
          "limit"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Order"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Всего заказов (с учётом `status`)."
          },
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      },
      "OrderResponse": {
        "type": "object",
        "required": [
          "ok",
          "order"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "order": {
            "$ref": "#/components/schemas/Order"
          }
        }
      },
      "TopupsList": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "items",
          "meta"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "kind": {
            "type": "string",
            "const": "topup"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TopupCategory"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/CursorMeta"
          }
        }
      },
      "TopupOffersResponse": {
        "type": "object",
        "required": [
          "ok",
          "category_id",
          "name",
          "note",
          "offers",
          "fields"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "category_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "note": {
            "$ref": "#/components/schemas/CatalogNote"
          },
          "offers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TopupOffer"
            }
          },
          "fields": {
            "type": "array",
            "description": "Что спросить у покупателя; пустой список — ничего.",
            "items": {
              "$ref": "#/components/schemas/BuyerField"
            }
          }
        }
      },
      "ValidateIdGamesResponse": {
        "type": "object",
        "required": [
          "ok",
          "items"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "category_id",
                "fields"
              ],
              "properties": {
                "category_id": {
                  "type": "string"
                },
                "fields": {
                  "type": "array",
                  "description": "Ключи полей, которые проверяются.",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "ValidateIdResponse": {
        "type": "object",
        "required": [
          "ok",
          "category_id",
          "valid",
          "player_name"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "category_id": {
            "type": "string"
          },
          "valid": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` — игрок найден; `false` — такого игрока нет; `null` — проверить не удалось (это не «неверно»)."
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Имя игрока, если проверка его вернула."
          }
        }
      },
      "GiftcardsList": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "items",
          "meta"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "kind": {
            "type": "string",
            "const": "giftcard"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GiftcardCategory"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/CursorMeta"
          }
        }
      },
      "GiftcardCardsResponse": {
        "type": "object",
        "required": [
          "ok",
          "category_id",
          "name",
          "note",
          "offers"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "category_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "note": {
            "$ref": "#/components/schemas/CatalogNote"
          },
          "offers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GiftcardOffer"
            }
          }
        }
      },
      "GamekeysList": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "items",
          "meta"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "kind": {
            "type": "string",
            "const": "gamekey"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GamekeyGame"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/CursorMeta"
          }
        }
      },
      "GamekeyKeysResponse": {
        "type": "object",
        "required": [
          "ok",
          "game_id",
          "name",
          "region",
          "platform",
          "keys"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "game_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "region": {
            "$ref": "#/components/schemas/Region"
          },
          "platform": {
            "type": [
              "string",
              "null"
            ]
          },
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GamekeyOffer"
            }
          }
        }
      },
      "SteamRatesResponse": {
        "type": "object",
        "required": [
          "ok",
          "base",
          "currencies"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "base": {
            "type": "string",
            "const": "USD",
            "description": "В чём списываются деньги."
          },
          "currencies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SteamCurrencyRate"
            }
          }
        }
      },
      "StarsQuoteResponse": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "price_per_star",
          "min_amount",
          "max_amount"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "kind": {
            "type": "string",
            "const": "telegram_stars"
          },
          "price_per_star": {
            "$ref": "#/components/schemas/UnitPriceUsd",
            "description": "Цена одной звезды."
          },
          "min_amount": {
            "type": "integer",
            "minimum": 1,
            "description": "Наименьшее число звёзд в заказе."
          },
          "max_amount": {
            "type": "integer",
            "minimum": 1,
            "description": "Наибольшее число звёзд в заказе (с учётом потолка суммы одного заказа)."
          }
        }
      },
      "PremiumQuoteResponse": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "plans"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "kind": {
            "type": "string",
            "const": "telegram_premium"
          },
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PremiumPlan"
            }
          }
        }
      },
      "RecipientResponse": {
        "type": "object",
        "required": [
          "ok",
          "found",
          "name",
          "message"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "found": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` — можно покупать; `false` — нельзя (аккаунт не найден, уже есть Premium, это канал или бот — см. `message`); `null` — проверить сейчас нельзя."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Имя получателя в Telegram, если найден."
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "Пояснение по-русски, если купить нельзя или проверка не удалась."
          }
        }
      },
      "WebhookResponse": {
        "type": "object",
        "required": [
          "ok",
          "webhook"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "webhook": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Webhook"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "WebhookSecretResponse": {
        "type": "object",
        "required": [
          "ok",
          "secret"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "secret": {
            "type": "string",
            "pattern": "^whsec_[0-9a-f]{32}$"
          }
        }
      },
      "WebhookTestResponse": {
        "type": "object",
        "required": [
          "ok",
          "status",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Ответил ли ваш адрес кодом 2xx."
          },
          "status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP-статус ответа; null — ответа не было."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Что пошло не так."
          }
        }
      },
      "DeliveriesResponse": {
        "type": "object",
        "required": [
          "ok",
          "items"
        ],
        "properties": {
          "ok": {
            "$ref": "#/components/schemas/Ok"
          },
          "items": {
            "type": "array",
            "maxItems": 50,
            "description": "Последние 50 доставок, новые сверху.",
            "items": {
              "$ref": "#/components/schemas/Delivery"
            }
          }
        }
      }
    },
    "examples": {
      "ErrorInvalidPlayerId": {
        "summary": "Не указан ID игрока",
        "value": {
          "ok": false,
          "error": "Укажите «Player ID»",
          "code": "invalid_fields",
          "field": "player_id"
        }
      },
      "ErrorInvalidUsername": {
        "summary": "Username неверного вида",
        "value": {
          "ok": false,
          "error": "«Username в Telegram»: от 5 до 32 символов — латинские буквы, цифры и _",
          "code": "invalid_fields",
          "field": "telegram_username"
        }
      },
      "ErrorItemUnavailable": {
        "summary": "Не продаётся",
        "value": {
          "ok": false,
          "error": "Этот номинал сейчас недоступен",
          "code": "product_unavailable"
        }
      },
      "ErrorStarsUnavailable": {
        "summary": "Звёзды не продаются",
        "value": {
          "ok": false,
          "error": "Звёзды сейчас не продаются",
          "code": "product_unavailable"
        }
      },
      "ErrorOutOfStock": {
        "summary": "Не хватает остатка",
        "value": {
          "ok": false,
          "error": "Столько нет в наличии: осталось 3",
          "code": "out_of_stock"
        }
      },
      "ErrorQuantityRange": {
        "summary": "Количество вне пределов",
        "value": {
          "ok": false,
          "error": "Максимальное количество — 100",
          "code": "quantity_out_of_range",
          "min": 1,
          "max": 100
        }
      },
      "ErrorOrderLimit": {
        "summary": "Больше потолка заказа",
        "value": {
          "ok": false,
          "error": "Не больше $200.00 за один заказ",
          "code": "order_limit"
        }
      },
      "ErrorRecipientNotFound": {
        "summary": "Аккаунт не найден",
        "value": {
          "ok": false,
          "error": "Аккаунт @example_user в Telegram не найден",
          "code": "recipient_not_found"
        }
      },
      "ErrorNotAUser": {
        "summary": "Канал или бот",
        "value": {
          "ok": false,
          "error": "Этот username принадлежит каналу или боту",
          "code": "not_a_user"
        }
      },
      "TopupCreatedResponse": {
        "summary": "PUBG Mobile 60 UC — заказ принят",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000123",
            "status": "pending",
            "kind": "topup",
            "product_id": "topup:pubg_mobile_auto",
            "item_id": "60_uc",
            "product_name": "PUBG Mobile (Auto)",
            "item_name": "60 UC",
            "quantity": 1,
            "amount": null,
            "currency": null,
            "fields": [
              {
                "key": "player_id",
                "label": "Player ID",
                "value": "5123456789"
              }
            ],
            "price_usd": "0.97",
            "total_usd": "0.97",
            "delivery": null,
            "failure_reason": null,
            "telegram": null,
            "idempotency_key": "my-order-42",
            "channel": "api",
            "created_at": "2026-10-21T10:00:00.000Z",
            "updated_at": "2026-10-21T10:00:00.000Z",
            "completed_at": null
          }
        }
      },
      "TopupCompletedResponse": {
        "summary": "PUBG Mobile 60 UC — выполнен",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000123",
            "status": "completed",
            "kind": "topup",
            "product_id": "topup:pubg_mobile_auto",
            "item_id": "60_uc",
            "product_name": "PUBG Mobile (Auto)",
            "item_name": "60 UC",
            "quantity": 1,
            "amount": null,
            "currency": null,
            "fields": [
              {
                "key": "player_id",
                "label": "Player ID",
                "value": "5123456789"
              }
            ],
            "price_usd": "0.97",
            "total_usd": "0.97",
            "delivery": {
              "codes": [],
              "text": null
            },
            "failure_reason": null,
            "telegram": null,
            "idempotency_key": "my-order-42",
            "channel": "api",
            "created_at": "2026-10-21T10:00:00.000Z",
            "updated_at": "2026-10-21T10:00:05.000Z",
            "completed_at": "2026-10-21T10:00:05.000Z"
          }
        }
      },
      "GiftcardCreatedResponse": {
        "summary": "A-Cash 10 MYR × 2 — заказ принят",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000128",
            "status": "pending",
            "kind": "giftcard",
            "product_id": "giftcard:acash_my",
            "item_id": "10_myr",
            "product_name": "A-Cash",
            "item_name": "10 MYR",
            "quantity": 2,
            "amount": null,
            "currency": null,
            "fields": [],
            "price_usd": "2.86",
            "total_usd": "5.72",
            "delivery": null,
            "failure_reason": null,
            "telegram": null,
            "idempotency_key": "cards-acash-7",
            "channel": "api",
            "created_at": "2026-10-21T11:00:00.000Z",
            "updated_at": "2026-10-21T11:00:00.000Z",
            "completed_at": null
          }
        }
      },
      "GamekeyCreatedResponse": {
        "summary": "Ключ 7 Days to Die — заказ принят",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000129",
            "status": "pending",
            "kind": "gamekey",
            "product_id": "gamekey:7_days_to_die_global",
            "item_id": "7_days_to_die",
            "product_name": "7 Days to Die",
            "item_name": "7 Days to Die",
            "quantity": 1,
            "amount": null,
            "currency": null,
            "fields": [],
            "price_usd": "18.07",
            "total_usd": "18.07",
            "delivery": null,
            "failure_reason": null,
            "telegram": null,
            "idempotency_key": "key-7dtd-1",
            "channel": "api",
            "created_at": "2026-10-21T11:10:00.000Z",
            "updated_at": "2026-10-21T11:10:00.000Z",
            "completed_at": null
          }
        }
      },
      "SteamCreatedResponse": {
        "summary": "Steam 500 RUB — заказ принят",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000124",
            "status": "pending",
            "kind": "steam_topup",
            "product_id": "steam_topup:wallet",
            "item_id": "RUB",
            "product_name": "Steam",
            "item_name": "500 RUB",
            "quantity": 1,
            "amount": 500,
            "currency": "RUB",
            "fields": [
              {
                "key": "steamLogin",
                "label": "Логин Steam",
                "value": "example_login"
              }
            ],
            "price_usd": "6.58",
            "total_usd": "6.58",
            "delivery": null,
            "failure_reason": null,
            "telegram": null,
            "idempotency_key": "steam-500-rub-1",
            "channel": "api",
            "created_at": "2026-10-21T10:05:00.000Z",
            "updated_at": "2026-10-21T10:05:00.000Z",
            "completed_at": null
          }
        }
      },
      "StarsCreatedResponse": {
        "summary": "500 звёзд — заказ принят",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000125",
            "status": "pending",
            "kind": "telegram",
            "product_id": "telegram:stars",
            "item_id": "stars",
            "product_name": "Telegram Stars",
            "item_name": "500 звёзд",
            "quantity": 1,
            "amount": 500,
            "currency": null,
            "fields": [
              {
                "key": "username",
                "label": "Username в Telegram",
                "value": "example_user"
              }
            ],
            "price_usd": "8.80",
            "total_usd": "8.80",
            "delivery": null,
            "failure_reason": null,
            "telegram": {
              "stage": "queued",
              "test": false
            },
            "idempotency_key": "stars-500-1",
            "channel": "api",
            "created_at": "2026-10-21T10:10:00.000Z",
            "updated_at": "2026-10-21T10:10:00.000Z",
            "completed_at": null
          }
        }
      },
      "PremiumCreatedResponse": {
        "summary": "Premium на 3 месяца — заказ принят",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000126",
            "status": "pending",
            "kind": "telegram",
            "product_id": "telegram:premium",
            "item_id": "3",
            "product_name": "Telegram Premium",
            "item_name": "3 месяца",
            "quantity": 1,
            "amount": null,
            "currency": null,
            "fields": [
              {
                "key": "username",
                "label": "Username в Telegram",
                "value": "example_user"
              }
            ],
            "price_usd": "12.66",
            "total_usd": "12.66",
            "delivery": null,
            "failure_reason": null,
            "telegram": {
              "stage": "queued",
              "test": false
            },
            "idempotency_key": "premium-3m-1",
            "channel": "api",
            "created_at": "2026-10-21T10:20:00.000Z",
            "updated_at": "2026-10-21T10:20:00.000Z",
            "completed_at": null
          }
        }
      },
      "PremiumCompletedResponse": {
        "summary": "Premium на 3 месяца — выполнен",
        "value": {
          "ok": true,
          "order": {
            "order_id": "ED-000126",
            "status": "completed",
            "kind": "telegram",
            "product_id": "telegram:premium",
            "item_id": "3",
            "product_name": "Telegram Premium",
            "item_name": "3 месяца",
            "quantity": 1,
            "amount": null,
            "currency": null,
            "fields": [
              {
                "key": "username",
                "label": "Username в Telegram",
                "value": "example_user"
              }
            ],
            "price_usd": "12.66",
            "total_usd": "12.66",
            "delivery": {
              "codes": [],
              "text": "Telegram Premium на 3 месяца подарен @example_user"
            },
            "failure_reason": null,
            "telegram": {
              "stage": "delivered",
              "test": false
            },
            "idempotency_key": "premium-3m-1",
            "channel": "api",
            "created_at": "2026-10-21T10:20:00.000Z",
            "updated_at": "2026-10-21T10:21:30.000Z",
            "completed_at": "2026-10-21T10:21:30.000Z"
          }
        }
      },
      "EventOrderProcessing": {
        "summary": "Steam 500 RUB выполняется",
        "value": {
          "id": "evt_8a4c1e7f2b9d6035",
          "type": "order.processing",
          "created_at": "2026-10-21T10:05:03.000Z",
          "data": {
            "order": {
              "order_id": "ED-000124",
              "status": "processing",
              "kind": "steam_topup",
              "product_id": "steam_topup:wallet",
              "item_id": "RUB",
              "product_name": "Steam",
              "item_name": "500 RUB",
              "quantity": 1,
              "amount": 500,
              "currency": "RUB",
              "fields": [
                {
                  "key": "steamLogin",
                  "label": "Логин Steam",
                  "value": "example_login"
                }
              ],
              "price_usd": "6.58",
              "total_usd": "6.58",
              "delivery": null,
              "failure_reason": null,
              "telegram": null,
              "idempotency_key": "steam-500-rub-1",
              "channel": "api",
              "created_at": "2026-10-21T10:05:00.000Z",
              "updated_at": "2026-10-21T10:05:03.000Z",
              "completed_at": null
            }
          }
        }
      },
      "EventOrderCompleted": {
        "summary": "PUBG Mobile 60 UC выполнен",
        "value": {
          "id": "evt_3b1f6c2a9d8e4f70",
          "type": "order.completed",
          "created_at": "2026-10-21T10:00:05.000Z",
          "data": {
            "order": {
              "order_id": "ED-000123",
              "status": "completed",
              "kind": "topup",
              "product_id": "topup:pubg_mobile_auto",
              "item_id": "60_uc",
              "product_name": "PUBG Mobile (Auto)",
              "item_name": "60 UC",
              "quantity": 1,
              "amount": null,
              "currency": null,
              "fields": [
                {
                  "key": "player_id",
                  "label": "Player ID",
                  "value": "5123456789"
                }
              ],
              "price_usd": "0.97",
              "total_usd": "0.97",
              "delivery": {
                "codes": [],
                "text": null
              },
              "failure_reason": null,
              "telegram": null,
              "idempotency_key": "my-order-42",
              "channel": "api",
              "created_at": "2026-10-21T10:00:00.000Z",
              "updated_at": "2026-10-21T10:00:05.000Z",
              "completed_at": "2026-10-21T10:00:05.000Z"
            }
          }
        }
      },
      "EventOrderRefunded": {
        "summary": "Steam 500 RUB — деньги вернулись",
        "value": {
          "id": "evt_e2a9b4c8d1f07356",
          "type": "order.refunded",
          "created_at": "2026-10-21T10:05:41.000Z",
          "data": {
            "order": {
              "order_id": "ED-000124",
              "status": "refunded",
              "kind": "steam_topup",
              "product_id": "steam_topup:wallet",
              "item_id": "RUB",
              "product_name": "Steam",
              "item_name": "500 RUB",
              "quantity": 1,
              "amount": 500,
              "currency": "RUB",
              "fields": [
                {
                  "key": "steamLogin",
                  "label": "Логин Steam",
                  "value": "example_login"
                }
              ],
              "price_usd": "6.58",
              "total_usd": "6.58",
              "delivery": null,
              "failure_reason": "Steam не принял данные: проверьте логин и сумму",
              "telegram": null,
              "idempotency_key": "steam-500-rub-1",
              "channel": "api",
              "created_at": "2026-10-21T10:05:00.000Z",
              "updated_at": "2026-10-21T10:05:41.000Z",
              "completed_at": null
            }
          }
        }
      },
      "EventWebhookTest": {
        "summary": "Проверка вебхука",
        "value": {
          "id": "evt_5c2e9a1f7b3d8e60",
          "type": "webhook.test",
          "created_at": "2026-10-21T09:00:00.000Z",
          "data": {}
        }
      }
    }
  }
}
