{"openapi":"3.1.0","info":{"title":"Donli API","version":"1"},"servers":[{"url":"https://donli.gg"}],"tags":[{"name":"Начало работы","description":"Donli API — доступ к профилю и донатам стримера: история, новые донаты в реальном времени и управление показом в\nэфире.\n\nМетоды API доступны по адресам `https://donli.gg/api/integrations/…` и принимают ключ в заголовке `Authorization: Bearer <ключ>`.\nТела ответов — JSON, время — UTC в формате ISO 8601 с `Z`.\n\n# Ключ из кабинета или OAuth\n\n**Ключ из кабинета** — стример копирует его в настройках, в разделе «Интеграция», и вставляет в ваш сервис. Передавайте\nего как есть; перевыпуск ключа сразу отключает прежний.\n\n**OAuth 2.0** — стример подтверждает подключение на экране согласия, вы получаете access-токен на 10 минут и\nrefresh-токен. Приложение нужно зарегистрировать — см. «Регистрация приложения».\n\n| | Ключ из кабинета | OAuth 2.0 |\n|---|---|---|\n| Кому подходит | Своему боту или скрипту стримера | Сервису, к которому подключаются многие стримеры |\n| Что делает стример | Копирует ключ и вставляет к вам | Нажимает «Разрешить» на экране согласия |\n| Регистрация у нас | Не нужна | Нужна |\n| Права | Все | Те, на которые согласился стример |\n\n> Если ваш сервис работает с чужими стримерами, используйте OAuth: стример видит на экране согласия, какие права даёт\n> вашему приложению, и может отключить его отдельно от других.\n\nДальше в документации «подключение» — ключ из кабинета или OAuth-подключение стримера к вашему приложению, а «ключ»\nв заголовке — и ключ из кабинета, и access-токен OAuth.\n\n# Права\n\n| Право | Что даёт |\n|---|---|\n| `profile:read` | Профиль стримера: ник, аватар, ссылка на страницу донатов |\n| `donations:read` | Список донатов и подписка на новые |\n| `donations:manage` | Одобрять и отклонять на модерации, пропускать показ, показывать повторно |\n\nКакое право нужно методу, написано в справочнике у каждого метода, в блоке Security, у варианта `oauth2`. Ключу из\nкабинета доступны все методы.\n\n# Первый запрос\n\nУзнайте, чей у вас ключ:\n\n```bash\ncurl https://donli.gg/api/integrations/profiles/me \\\n  -H \"Authorization: Bearer $DONLI_KEY\"\n```\n\n```json\n{\n  \"id\": \"0199d2a4-5c3e-7c41-9a51-3f0e2b7c8d10\",\n  \"nickname\": \"streamer\",\n  \"avatarUrl\": \"https://donli.gg/contents/avatars/0199d2a4-5c3e-7c41-9a51-3f0e2b7c8d10.png?v=1760000000000\",\n  \"donationPageUrl\": \"https://donli.gg/d/streamer\"\n}\n```\n\n`id` — тот же идентификатор стримера, что в поле `streamerId` у донатов.\n","x-slug":"getting-started","x-traitTag":true},{"name":"Регистрация приложения","description":"Чтобы подключать стримеров через OAuth 2.0, приложение регистрируют у нас. Ключу из кабинета регистрация не нужна.\n\n# Куда писать\n\nВ поддержку: Telegram [@donli_support](https://t.me/donli_support) или почта\n[support@donli.gg](mailto:support@donli.gg). Тем же каналом присылайте правки после регистрации.\n\n# Что прислать\n\n| Что | Подробности |\n|---|---|\n| Название | Стример увидит его на экране согласия и в списке подключённых приложений |\n| Логотип | Файлом: PNG, JPG, GIF или WEBP, до 5 МБ. Без логотипа покажем первую букву названия |\n| Адреса возврата (`redirect_uri`) | Список точных адресов, куда вернуть стримера после согласия. Только `https`; для разработки можно `http://127.0.0.1` и `http://localhost` |\n| Тип приложения | Публичное или с секретом — см. ниже |\n| Права | Какие из `profile:read`, `donations:read`, `donations:manage` нужны приложению. Без `profile:read` приложение не узнает, какой стример подключился |\n\n**Адреса возврата сравниваются целиком**, включая путь. Исключение — `http://127.0.0.1`: порт может быть любым. У\n`http://localhost` порт сравнивается точно.\n\n**Тип приложения:**\n\n- **Публичное** — всё работает в браузере или на устройстве пользователя, секрет хранить негде. Приложение\n  предъявляет только `client_id`.\n- **С секретом** — у приложения есть свой сервер. Обмен кода и обновление токенов идут с сервера с `client_secret`.\n\nЕсли нужно и то и другое, регистрируйте два приложения. Тип после регистрации не меняется — смена типа означает\nновую регистрацию.\n\n# Что вы получите\n\n`client_id`, а приложению с секретом — ещё `client_secret`. Секрет придёт тем же каналом, что и заявка. Если он\nутёк или потерялся, попросите перевыпустить: старый перестанет работать сразу, выданные стримерам токены продолжат\nработать.\n\n# Изменения\n\nНазвание, логотип, адреса возврата и права меняются по заявке тем же каналом.\n\n# Тестовый контур\n\nДля разработки можем зарегистрировать приложение и на тестовом контуре, где донаты проходят без настоящих денег.\nПопросите об этом в заявке — адрес пришлём в ответ.\n","x-slug":"registration","x-traitTag":true},{"name":"OAuth 2.0","description":"Стандартный Authorization Code с PKCE (RFC 6749, RFC 7636). PKCE обязателен всем приложениям, в том числе с\nсекретом.\n\n| | Адрес |\n|---|---|\n| Согласие стримера | `https://donli.gg/oauth2/authorize` |\n| Токены | `POST https://donli.gg/api/integrations/oauth2/token` |\n| Отзыв | `POST https://donli.gg/api/integrations/oauth2/revoke` |\n\nМетоды токенов и отзыва принимают `application/x-www-form-urlencoded`. Как и методы API, они разрешают запросы с любого\nсайта (CORS).\n\n# Подключение стримера\n\n**1. Отправьте стримера на экран согласия.** Сгенерируйте `code_verifier` — случайную строку из 43–128 символов\n`A–Z a–z 0–9 - . _ ~` — и `code_challenge = BASE64URL(SHA256(code_verifier))` без `=` на конце.\n\n| Параметр | Значение |\n|---|---|\n| `response_type` | `code` |\n| `client_id` | Ваш `client_id` |\n| `redirect_uri` | Один из зарегистрированных адресов, символ в символ |\n| `scope` | Весь набор прав, зарегистрированный для приложения, через пробел |\n| `state` | Случайная строка — вернётся вам без изменений, сверьте её |\n| `code_challenge` | Из `code_verifier` |\n| `code_challenge_method` | `S256` |\n\nПараметры кодируйте как в любом URL — пробелы в `scope` становятся `%20`:\n\n```text\nhttps://donli.gg/oauth2/authorize?response_type=code&client_id=<client_id>&redirect_uri=https%3A%2F%2Fapp.example.com%2Fdonli%2Fcallback&scope=profile%3Aread%20donations%3Aread%20donations%3Amanage&state=<state>&code_challenge=<code_challenge>&code_challenge_method=S256\n```\n\nЕсли стример не вошёл в Donli, он сначала войдёт, а потом сам вернётся на экран согласия. Если подключение к вашему\nприложению у стримера уже есть и права не менялись, экрана не будет — код придёт сразу. Если стример отключал\nприложение, экран покажется снова.\n\n**2. Получите код.** После «Разрешить» стример вернётся на `redirect_uri?code=…&state=…`. Код одноразовый — обменяйте его\nсразу.\n\nЕсли подключения ещё нет и стример нажал «Отклонить», придёт `?error=access_denied&state=…`. Неверный или пустой\n`scope` — `?error=invalid_scope&state=…`.\n\n**3. Обменяйте код на токены.**\n\nЗапрос публичного приложения:\n\n```bash\ncurl https://donli.gg/api/integrations/oauth2/token \\\n  -d grant_type=authorization_code \\\n  -d code=<код> \\\n  -d redirect_uri=https://app.example.com/donli/callback \\\n  -d code_verifier=<code_verifier> \\\n  -d client_id=<client_id>\n```\n\nЗапрос приложения с секретом — те же параметры, плюс секрет. Предъявить его можно одним из двух способов.\n\nЗаголовком `Authorization: Basic` с парой `client_id:client_secret` в Base64 — тогда `client_id` в теле не нужен\n(в `curl` заголовок собирает `-u`):\n\n```bash\ncurl https://donli.gg/api/integrations/oauth2/token \\\n  -u <client_id>:<client_secret> \\\n  -d grant_type=authorization_code \\\n  -d code=<код> \\\n  -d redirect_uri=https://app.example.com/donli/callback \\\n  -d code_verifier=<code_verifier>\n```\n\nИли в теле — `client_id` и `client_secret` двумя полями, без заголовка:\n\n```bash\ncurl https://donli.gg/api/integrations/oauth2/token \\\n  -d grant_type=authorization_code \\\n  -d code=<код> \\\n  -d redirect_uri=https://app.example.com/donli/callback \\\n  -d code_verifier=<code_verifier> \\\n  -d client_id=<client_id> \\\n  -d client_secret=<client_secret>\n```\n\nДальше «предъявляет секрет» — любым из этих двух способов.\n\nОтвет:\n\n```json\n{\n  \"access_token\": \"q7Nv…\",\n  \"refresh_token\": \"Xk3p…\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 600,\n  \"scope\": \"profile:read donations:read donations:manage\"\n}\n```\n\n| Поле | Что это |\n|---|---|\n| `access_token` | Передавайте в `Authorization: Bearer` |\n| `refresh_token` | Храните — им обновляют access |\n| `expires_in` | Через сколько секунд access истечёт |\n| `scope` | Права подключения — те, на которые согласился стример. Если в `authorize` запрошена часть набора, здесь будет только она |\n\nНужно право, которого нет в `scope` (метод ответил `403` с `insufficient_scope`), — проведите стримера через экран\nсогласия с этим правом в `scope`; если стример откажет, код придёт со старым набором.\n\nСервису на многих стримеров нужно знать, кто подключился: сразу после обмена запросите `GET /api/integrations/profiles/me`\nи сохраните подключение под `id` стримера. Это нужно один раз, а не перед каждым запросом.\n\n**4. Ходите в API** с `Authorization: Bearer <access_token>`.\n\n# Обновление токенов\n\nAccess живёт 10 минут. Обновляйте его refresh-токеном.\n\nЗапрос публичного приложения:\n\n```bash\ncurl https://donli.gg/api/integrations/oauth2/token \\\n  -d grant_type=refresh_token \\\n  -d refresh_token=<refresh_token> \\\n  -d client_id=<client_id>\n```\n\nПриложение с секретом предъявляет секрет, как при обмене кода. Ответ — в том же формате, что при обмене кода.\n\n- **Каждое обновление возвращает новый refresh-токен** — дальше используйте его.\n- **Обновляйте одним запросом на всех.** Если несколько запросов или вкладок одновременно увидели истёкший access,\n  обновление должен сделать один, остальные — дождаться его результата (общий промис, во вкладках —\n  [Web Locks](https://developer.mozilla.org/docs/Web/API/Web_Locks_API), у нескольких процессов — общая блокировка).\n- **Ответ потерялся — повторите.** Повтор с тем же refresh-токеном в течение минуты вернёт тот же ответ.\n- **Refresh живёт 180 дней с последнего обновления.** Пока приложение обновляет токены, подключение не истекает.\n\n**`invalid_grant` значит, что подключения больше нет:** истёк refresh, стример отключил приложение или приложение\nудалили. Отправьте стримера на экран согласия заново (шаг 1).\n\n# Отзыв\n\nКогда стример отключает Donli в вашем сервисе, отзовите подключение:\n\n```bash\ncurl https://donli.gg/api/integrations/oauth2/revoke \\\n  -d token=<refresh_token> \\\n  -d client_id=<client_id>\n```\n\nПриложение с секретом предъявляет секрет, как при обмене кода.\n\nОтзыв гасит подключение целиком: все токены этого стримера в вашем приложении перестают работать сразу. Стример\nможет отключить приложение и сам — в настройках Donli, в разделе «Интеграция». После отключения методы API отвечают\n`401` с `WWW-Authenticate: Bearer error=\"invalid_token\"`.\n\n# Ошибки токенов и отзыва\n\nМетоды `/token` и `/revoke` отвечают ошибками в формате OAuth (RFC 6749), а не общим форматом API:\n\n```json\n{ \"error\": \"invalid_grant\", \"error_description\": \"…\" }\n```\n\n| `error` | Что значит |\n|---|---|\n| `invalid_grant` | Код использован или истёк, подключения больше нет — начните с шага 1 |\n| `invalid_client` | Неизвестный `client_id` или неверный секрет; приложение с секретом пришло без секрета; публичное приложение меняет код без `code_verifier` |\n| `invalid_request` | Не хватает параметра или он неверен |\n","x-slug":"oauth","x-traitTag":true},{"name":"Подписка на новые донаты","description":"Новые донаты и изменения их статуса приходят по WebSocket через [Centrifugo](https://centrifugal.dev). Нужно право\n`donations:read`.\n\n# Подключение\n\n1. Получите токен подключения к Centrifugo: `POST /api/integrations/realtime-tokens` с вашим ключом, без тела. Ответ —\n   `{\"token\": \"…\"}`, токен живёт 10 минут.\n2. Подключитесь официальным SDK Centrifugo к `wss://donli.gg/connection/websocket`. SDK есть для\n   [JavaScript](https://github.com/centrifugal/centrifuge-js), [Go](https://github.com/centrifugal/centrifuge-go),\n   [Python](https://github.com/centrifugal/centrifuge-python), [Java](https://github.com/centrifugal/centrifuge-java),\n   [Swift](https://github.com/centrifugal/centrifuge-swift) и [Dart](https://github.com/centrifugal/centrifuge-dart).\n3. Отдайте SDK функцию, которая получает токен, — продлевать его SDK будет сам.\n\n```javascript\nimport { Centrifuge, UnauthorizedError } from 'centrifuge';\n\nconst client = new Centrifuge('wss://donli.gg/connection/websocket', {\n    getToken: async () => {\n        // donliFetch — ваш клиент API: подставляет действующий access-токен, на 401 обновляет его и повторяет запрос.\n        const res = await donliFetch('/api/integrations/realtime-tokens', { method: 'POST' });\n        if (res.status === 401 || res.status === 403) {\n            // 401 сюда доходит, только если обновление токенов не помогло: подключения больше нет. 403 — нет права.\n            throw new UnauthorizedError();\n        }\n        if (!res.ok) {\n            throw new Error(`token: ${res.status}`); // временная ошибка — SDK повторит позже\n        }\n        return (await res.json()).token;\n    },\n});\n\nclient.on('subscribed', () => catchUp());            // после подключения и каждого переподключения\nclient.on('publication', ctx => onEvent(ctx.data)); // ctx.data — сообщение канала, объект\nclient.connect();\n```\n\n**Подписываться самому не нужно.** Канал донатов стримера уже записан в токене, сервер подписывает соединение сам.\nНе создавайте подписок (`newSubscription`) — слушайте события самого клиента: `subscribed` и `publication`. Имя\nканала приходит в `ctx.channel`; не собирайте его сами.\n\nДержите одно соединение на подключение: число одновременных соединений на подключение ограничено.\n\n# Догон после переподключения\n\nКанал доставляет события в реальном времени; то, что изменилось, пока вы были отключены, дочитывайте списком. На\nкаждое `subscribed` — и после первого подключения, и после каждого переподключения — дочитайте список донатов\n(`GET /api/integrations/donations`) от новых к старым. Листайте до самого старого из показываемых вами донатов в\nстатусе `MODERATION`, `DELAYED` или `QUEUED`: его статус мог измениться, пока вы были отключены. Если таких нет — до\nпоследнего доната, который уже знаете. При первом подключении берите столько истории, сколько вам нужно.\n\nСобытия одного стримера приходят в том порядке, в каком менялись донаты, в том числе на ваши собственные действия.\nСобытия могут прийти, пока идёт догон. Каждое сообщение несёт донат целиком, поэтому склеивайте по `id` и\nоставляйте версию с бóльшим `statusChangedAt`.\n\n# Отключение\n\nЕсли подключение отозвано — стример отключил ваше приложение или перевыпустил ключ из кабинета, которым вы\nпользуетесь, — сервер разрывает соединение, и SDK не переподключается. Для OAuth обновите токены: `invalid_grant`\nзначит, что стримера нужно провести через экран согласия заново. Для ключа из кабинета — попросите у стримера новый\nключ.\n\n# Сообщения\n\nКаждое сообщение — `{type, payload}`: `type` — что произошло, `payload` — донат после события в той же форме, что в\nсписке донатов.\n\n<SchemaDefinition schemaRef=\"#/components/schemas/DonationEvent\" />\n\n| `type` | Что произошло | Статус после |\n|---|---|---|\n| `CREATED` | Донат оплачен и появился у стримера | `MODERATION`, `DELAYED` или `QUEUED` — по настройкам модерации |\n| `APPROVED` | Одобрен на модерации, в том числе во время задержки | `QUEUED` |\n| `REJECTED` | Отклонён на модерации | `BLOCKED` |\n| `RELEASED` | Истекла задержка показа | `QUEUED` |\n| `SHOW_STARTED` | Начался показ в эфире | `STARTED` |\n| `SHOW_FINISHED` | Показ закончился | `SHOWN` |\n| `SKIPPED` | Пропущен — по одному или «Пропустить все» | `SKIPPED` |\n| `REPEATED` | Поставлен на повторный показ, первым в очереди | `QUEUED` |\n\nНовые типы событий могут появиться без предупреждения — пропускайте незнакомые.\n\n# Статусы доната\n\n| `status` | Что значит |\n|---|---|\n| `MODERATION` | Ждёт решения модерации |\n| `DELAYED` | Ждёт задержки показа до `delayedUntil`; модерация может решить раньше |\n| `QUEUED` | В очереди на показ |\n| `STARTED` | Показ начался. Если показ оборвался, донат остаётся в этом статусе — его можно повторить (`REPEAT`) или пропустить (`SKIP`) |\n| `SHOWN` | Показан |\n| `SKIPPED` | Пропущен; можно показать повторно |\n| `BLOCKED` | Отклонён модерацией; можно показать повторно |\n","x-slug":"realtime","x-traitTag":true},{"name":"Правила и ошибки","description":"# Совместимость\n\nAPI развивается без смены версии, пока изменения ничего не ломают:\n\n- в ответах и событиях появляются новые поля — **игнорируйте незнакомые поля**;\n- в канале появляются новые типы событий — **пропускайте незнакомые типы**;\n- появляются новые методы.\n\nНовое значение `status` — ломающее изменение: оно выйдет только с новой версией API.\n\n# Запросы из браузера\n\nМетоды API принимают запросы из браузера с любого сайта; заголовки `Retry-After` и `WWW-Authenticate` доступны из\nJavaScript.\n\n# Формат ошибок\n\nМетоды API отвечают ошибкой в общем формате:\n\n```json\n{\n  \"message\": \"Ошибка валидации\",\n  \"details\": [\n    { \"fieldName\": \"cursor\", \"errorMessage\": \"lastAt и lastId передаются вместе\" }\n  ]\n}\n```\n\n`message` — текст для человека: показывайте или пишите в лог, но не разбирайте программно. `details` есть не у\nвсех ошибок: у `400` это список полей с ошибками (`cursor` — пара `lastAt` и `lastId`), у `409` — текущий статус\nдоната. Методы `/oauth2/token` и `/oauth2/revoke` отвечают в формате OAuth — см. «OAuth 2.0».\n\n| Код | Что значит | Что делать |\n|---|---|---|\n| `400` | Неверный запрос: параметр, тело, неполный курсор | Исправить запрос по `details` |\n| `401` | Нет ключа или он недействителен | См. ниже |\n| `403` | У подключения нет права на этот метод | Для OAuth — провести стримера через экран согласия с нужным правом |\n| `404` | Такого доната у стримера нет | — |\n| `409` | Действие недоступно в текущем статусе доната | См. «Повтор действия» |\n| `429` | Превышен лимит запросов; запрос не выполнен | Подождать `Retry-After` секунд и повторить |\n| `5xx` | Временный сбой на нашей стороне | Повторить позже; действие могло пройти — см. «Повтор действия» |\n\n**`401`** приходит с заголовком `WWW-Authenticate`:\n\n- `Bearer` — ключ не передан;\n- `Bearer error=\"invalid_token\"` — ключ неверен, подключение отозвано или access-токен истёк. Для OAuth обновите\n  токены и повторите запрос; если обновление вернуло `invalid_grant`, подключения больше нет. Для ключа из\n  кабинета — попросите у стримера новый.\n\n# Действия с донатом\n\n| Действие | Из каких статусов | Статус после | Событие |\n|---|---|---|---|\n| `APPROVE` | `MODERATION`, `DELAYED` | `QUEUED` | `APPROVED` |\n| `REJECT` | `MODERATION`, `DELAYED` | `BLOCKED` | `REJECTED` |\n| `SKIP` | `QUEUED`, `STARTED`, `SHOWN` | `SKIPPED` | `SKIPPED` |\n| `REPEAT` | `STARTED`, `SHOWN`, `SKIPPED`, `BLOCKED` | `QUEUED`, первым в очереди | `REPEATED` |\n\n**Пропустить все** (`POST /api/integrations/donations/skip-all`) пропускает донаты в статусах `MODERATION`,\n`DELAYED` и `QUEUED`. Если в теле передан `onAirDonationId` — донат, который вы сейчас видите в эфире, — его показ\nтоже пропускается; неизвестный или чужой `onAirDonationId` игнорируется. Тело можно не передавать.\n\n# Повтор действия\n\nЕсли ответ на действие потерялся, повтор `APPROVE`, `REJECT` и `SKIP` безопасен: если первый запрос дошёл, второй\nвернёт `409` с текущим статусом в `details.status`.\n\n```json\n{ \"message\": \"Действие недоступно в текущем статусе доната\", \"details\": { \"status\": \"QUEUED\" } }\n```\n\n**`409` со статусом, к которому вело ваше действие, значит, что первый запрос прошёл.** Между запросами донат мог\nпродвинуться дальше — одобренный успеть показаться (`STARTED`, `SHOWN`); это тоже значит, что первое действие прошло.\n\n**`REPEAT` и «Пропустить все» вслепую не повторяйте.** Повторный `REPEAT` после успешного первого может снова\nпоставить донат в очередь, повторное «Пропустить все» — пропустить донаты, пришедшие между запросами. Проверьте по\nсобытию в канале или по списку донатов, дошло ли первое действие.\n\n# Лимиты\n\n**60 запросов в минуту на подключение.** На `429` подождите столько секунд, сколько указано в заголовке\n`Retry-After`, и повторите запрос.\n\n# Время и страницы\n\n**Время в ответах** — UTC в ISO 8601 с `Z`: `2026-10-15T12:34:56.789123Z`.\n\n**Время в параметрах запроса** передавайте так же, с `Z`. Если передаёте со смещением (`+03:00`), кодируйте параметр\n(`%2B03:00`): незакодированный `+` превращается в пробел, и запрос вернёт `400`.\n\n**Страницы списка.** Ответ — `{content, nextCursor, hasNext}`, донаты от новых к старым по `createdAt`.\n\n- Следующую страницу запрашивайте с `lastAt = nextCursor.at` и `lastId = nextCursor.id`, передавая оба. `at`\n  передавайте строкой как пришло, не перегоняя через тип даты, — иначе потеряете точность и получите дубли или\n  пропуски на стыке страниц.\n- Период (`from`, `to`) и `size` передавайте на каждой странице.\n- На последней странице `nextCursor` нет, а `hasNext` — `false`.\n- `size` — от 1 до 50, по умолчанию 10; при большем вернётся 50.\n\n# Тексты донатера\n\n`donorName` и `content.message` приходят так, как их ввёл донатер: выводите их как текст, не как HTML.\n\n# Ссылки на файлы\n\n`content.url`, `content.coverUrl`, `avatarUrl` — абсолютные адреса файлов. Открываются без ключа и с любого сайта\n(CORS): годятся прямо в `<img>`, `<audio>`, `<video>` и для `fetch`. Берите адрес из ответа как есть — путь и домен\nмогут меняться.\n","x-slug":"rules","x-traitTag":true},{"name":"Профиль"},{"name":"Донаты"},{"name":"Подписка"}],"paths":{"/api/integrations/profiles/me":{"get":{"tags":["Профиль"],"summary":"Профиль стримера","description":"Стример, которому принадлежит ключ.","operationId":"getProfile","responses":{"200":{"description":"Профиль","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Profile"}}}},"401":{"description":"Нет ключа, ключ неверен или отозван, access-токен истёк","headers":{"WWW-Authenticate":{"description":"Bearer — ключ не передан; Bearer error=\"invalid_token\" — ключ неверен, подключение отозвано или access истёк","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"У подключения нет нужного права","headers":{"WWW-Authenticate":{"description":"Bearer error=\"insufficient_scope\"","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Превышен лимит запросов ключа","headers":{"Retry-After":{"description":"Через сколько секунд повторить","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearer":[]},{"oauth2":["profile:read"]}]}},"/api/integrations/donations":{"get":{"tags":["Донаты"],"summary":"Список донатов","description":"Новые сверху. Неоплаченных донатов в списке нет. Следующую страницу запрашивают с <code>lastAt</code> и <code>lastId</code> из\n <code>nextCursor</code> предыдущей.","operationId":"getDonations","parameters":[{"name":"lastAt","in":"query","description":"<code>nextCursor.at</code> предыдущей страницы; для первой страницы не передаётся","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"lastId","in":"query","description":"<code>nextCursor.id</code> предыдущей страницы; передаётся вместе с <code>lastAt</code>","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"size","in":"query","description":"размер страницы: по умолчанию 10, не больше 50","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"from","in":"query","description":"начало периода по <code>createdAt</code>, включительно","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"конец периода по <code>createdAt</code>, не включительно","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Страница донатов","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DonationList"}}}},"400":{"description":"Неполный курсор или неверный формат параметра","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Нет ключа, ключ неверен или отозван, access-токен истёк","headers":{"WWW-Authenticate":{"description":"Bearer — ключ не передан; Bearer error=\"invalid_token\" — ключ неверен, подключение отозвано или access истёк","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"У подключения нет нужного права","headers":{"WWW-Authenticate":{"description":"Bearer error=\"insufficient_scope\"","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Превышен лимит запросов ключа","headers":{"Retry-After":{"description":"Через сколько секунд повторить","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearer":[]},{"oauth2":["donations:read"]}]}},"/api/integrations/donations/{donationId}":{"patch":{"tags":["Донаты"],"summary":"Действие над донатом","description":"Одобрить или отклонить на модерации, пропустить показ, показать повторно.","operationId":"manageDonation","parameters":[{"name":"donationId","in":"path","description":"идентификатор доната","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"действие","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DonationActionRequest"}}},"required":true},"responses":{"200":{"description":"Действие выполнено"},"400":{"description":"Неизвестное действие или неверное тело запроса","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"У стримера нет такого доната","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Действие недоступно в текущем статусе доната; статус — в details.status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Нет ключа, ключ неверен или отозван, access-токен истёк","headers":{"WWW-Authenticate":{"description":"Bearer — ключ не передан; Bearer error=\"invalid_token\" — ключ неверен, подключение отозвано или access истёк","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"У подключения нет нужного права","headers":{"WWW-Authenticate":{"description":"Bearer error=\"insufficient_scope\"","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Превышен лимит запросов ключа","headers":{"Retry-After":{"description":"Через сколько секунд повторить","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearer":[]},{"oauth2":["donations:manage"]}]}},"/api/integrations/donations/skip-all":{"post":{"tags":["Донаты"],"summary":"Пропустить все донаты","description":"Пропускаются все донаты, ожидающие показа.","operationId":"skipAll","requestBody":{"description":"донат, который сервис видит в эфире: его показ тоже пропускается","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SkipAllRequest"}}}},"responses":{"200":{"description":"Донаты пропущены"},"400":{"description":"Неверное тело запроса","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Нет ключа, ключ неверен или отозван, access-токен истёк","headers":{"WWW-Authenticate":{"description":"Bearer — ключ не передан; Bearer error=\"invalid_token\" — ключ неверен, подключение отозвано или access истёк","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"У подключения нет нужного права","headers":{"WWW-Authenticate":{"description":"Bearer error=\"insufficient_scope\"","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Превышен лимит запросов ключа","headers":{"Retry-After":{"description":"Через сколько секунд повторить","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearer":[]},{"oauth2":["donations:manage"]}]}},"/api/integrations/realtime-tokens":{"post":{"tags":["Подписка"],"summary":"Токен realtime","description":"Токен подключения к Centrifugo с подпиской на канал донатов стримера.","operationId":"issueToken","responses":{"200":{"description":"Токен; срок короткий, продлевается через <code>getToken</code> SDK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RealtimeToken"}}}},"401":{"description":"Нет ключа, ключ неверен или отозван, access-токен истёк","headers":{"WWW-Authenticate":{"description":"Bearer — ключ не передан; Bearer error=\"invalid_token\" — ключ неверен, подключение отозвано или access истёк","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"У подключения нет нужного права","headers":{"WWW-Authenticate":{"description":"Bearer error=\"insufficient_scope\"","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Превышен лимит запросов ключа","headers":{"Retry-After":{"description":"Через сколько секунд повторить","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearer":[]},{"oauth2":["donations:read"]}]}}},"components":{"schemas":{"RealtimeToken":{"type":"object","properties":{"token":{"type":"string","description":"токен подключения к Centrifugo; канал донатов в нём уже указан, подписываться самому не нужно"}}},"SkipAllRequest":{"type":"object","properties":{"onAirDonationId":{"type":["string","null"],"format":"uuid","description":"донат, который сервис сейчас видит в эфире; <code>null</code> — показа нет"}}},"DonationActionRequest":{"type":"object","properties":{"action":{"type":"string","description":"действие над донатом","enum":["APPROVE","REJECT","REPEAT","SKIP"]}},"required":["action"]},"Profile":{"type":"object","description":"Стример, которому принадлежит ключ.","properties":{"id":{"type":"string","format":"uuid","description":"идентификатор стримера; не меняется"},"nickname":{"type":"string","description":"никнейм; стример может его сменить"},"avatarUrl":{"type":["string","null"],"description":"аватар; <code>null</code> — не загружен"},"donationPageUrl":{"type":"string","description":"страница донатов стримера"}}},"Donation":{"type":"object","description":"Донат: одна и та же форма в списке донатов и в событиях канала.","properties":{"id":{"type":"string","format":"uuid","description":"идентификатор доната"},"streamerId":{"type":"string","format":"uuid","description":"стример — получатель доната"},"createdAt":{"type":"string","format":"date-time","description":"момент появления доната у стримера: донат оплачен"},"amount":{"type":"number","description":"сумма, назначенная донатером, без комиссии"},"currency":{"type":"string","description":"трёхбуквенный код валюты, например <code>RUB</code>"},"feeCovered":{"type":"boolean","description":"донатер покрыл комиссию сверх суммы"},"donorName":{"type":["string","null"],"description":"ник донатера как введён; <code>null</code> — аноним"},"content":{"$ref":"#/components/schemas/DonationContent","description":"содержимое доната"},"media":{"description":"заказанный ролик; <code>null</code> — без заказа","oneOf":[{"$ref":"#/components/schemas/DonationMedia"},{"type":"null"}]},"status":{"type":"string","description":"статус показа","enum":["MODERATION","DELAYED","QUEUED","STARTED","SHOWN","SKIPPED","BLOCKED"]},"statusChangedAt":{"type":"string","format":"date-time","description":"момент перехода в текущий статус"},"delayedUntil":{"type":["string","null"],"format":"date-time","description":"когда донат покажется сам; есть только у статуса <code>DELAYED</code>"}}},"DonationContent":{"type":"object","properties":{"type":{"type":"string","description":"вид содержимого","enum":["TEXT","VOICE","VIDEO"]},"message":{"type":["string","null"],"description":"текст доната как введён; есть только у текстового"},"url":{"type":["string","null"],"description":"файл: озвучка текста, голосовое или кружок; <code>null</code> — играть нечего"},"coverUrl":{"type":["string","null"],"description":"обложка кружка; <code>null</code>, пока её нет"}}},"DonationMedia":{"type":"object","properties":{"url":{"type":"string","description":"канонический адрес ролика с меткой времени старта"}}},"DonationEvent":{"type":"object","description":"Сообщение канала донатов","properties":{"type":{"type":"string","description":"Что произошло с донатом; список может расшириться — незнакомые типы пропускайте","enum":["CREATED","APPROVED","REJECTED","RELEASED","SHOW_STARTED","SHOW_FINISHED","SKIPPED","REPEATED"]},"payload":{"$ref":"#/components/schemas/Donation","description":"Донат после события"}},"required":["payload","type"]},"Error":{"type":"object","properties":{"message":{"type":"string","description":"Описание ошибки для человека."},"details":{"description":"Подробности; форма зависит от ошибки: список ошибок полей у валидации, текущий статус у конфликта.","oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ErrorValidationDetails"}},{"$ref":"#/components/schemas/DonationStatusDetails"}]}}},"ErrorValidationDetails":{"type":"object","properties":{"fieldName":{"type":"string","description":"Поле или параметр запроса, не прошедший проверку."},"errorMessage":{"type":"string","description":"Что не так со значением."}}},"DonationStatusDetails":{"type":"object","properties":{"status":{"type":"string","description":"статус, в котором донат остался после отказа","enum":["MODERATION","DELAYED","QUEUED","STARTED","SHOWN","SKIPPED","BLOCKED"]}}},"Cursor":{"type":"object","description":"Позиция следующей страницы: метка и id последнего элемента текущей страницы. Какая метка — сказано в описании\n списка.","properties":{"at":{"type":"string","format":"date-time","description":"метка последнего элемента страницы"},"id":{"type":"string","format":"uuid","description":"id последнего элемента страницы; различает элементы с равной меткой"},"size":{"type":"integer","format":"int32","description":"размер этой страницы"}}},"DonationList":{"type":"object","properties":{"content":{"type":"array","description":"Элементы страницы.","items":{"$ref":"#/components/schemas/Donation"}},"nextCursor":{"$ref":"#/components/schemas/Cursor","description":"Позиция для запроса следующей страницы; нет у последней страницы."},"hasNext":{"type":"boolean","description":"Есть ли следующая страница."}}}},"securitySchemes":{"bearer":{"type":"http","description":"Ключ из кабинета стримера — как есть, без разбора.","scheme":"bearer"},"oauth2":{"type":"oauth2","description":"Подключение приложения стримером. Access-токен шлётся тем же заголовком Authorization: Bearer.","flows":{"authorizationCode":{"authorizationUrl":"https://donli.gg/oauth2/authorize","tokenUrl":"https://donli.gg/api/integrations/oauth2/token","refreshUrl":"https://donli.gg/api/integrations/oauth2/token","scopes":{"profile:read":"Видеть ваш профиль: ник, аватар и ссылку на страницу донатов","donations:read":"Видеть ваши донаты и сразу получать новые","donations:manage":"Управлять донатами: одобрять и отклонять, пропускать и повторять показ"}}}}}},"x-tagGroups":[{"name":"Руководства","tags":["Начало работы","Регистрация приложения","OAuth 2.0","Подписка на новые донаты","Правила и ошибки"]},{"name":"Справочник","tags":["Профиль","Донаты","Подписка"]}]}