Skip to main content
HTTP API Omniflow разделён на три префикса, которые никогда не делят между собой ни маршрутизатор, ни учётные данные, ни стек middleware. Эта страница про /v1/admin — поверхность, которую установка автоматизирует общим bearer-токеном из скрипта, задания CI или другого сервиса. Объединение любых двух из них означало бы, что один молча наследует middleware другого, а учётные данные клиента никогда не должны дотягиваться до операторского маршрута. Структурное разделение делает это свойством таблицы маршрутов, а не проверкой, о которой кто-то должен помнить, и есть тест, утверждающий, что клиентский маршрутизатор отвечает 404 на /v1/panel/auth/session, /v1/admin/orders и /v1/panel/admins.

Когда использовать /v1/admin, а не /v1/panel

Используйте /v1/admin, когда вызывающая сторона — не браузер. Здесь нет сессии, которую нужно установить, нет CSRF-токена, который нужно держать в согласии с ротацией, и нет хранилища cookie. Это единственный скриптуемый способ создать заказ, принять платёж, зарегистрировать возврат, скорректировать кошелёк, запустить импорт из Remnawave и поставить в очередь операцию выдачи доступа. Используйте /v1/panel, когда вызывающая сторона — человек, или когда вам нужно то, что /v1/admin не предоставляет: служба поддержки, маркетинговые кампании, редактор каталога, настройки установки, журнал аудита, разбор рисков. Это также единственная поверхность с авторизацией по отдельным возможностям: интеграция, которая должна уметь читать, но не оформлять возвраты, живёт за учётной записью оператора с узкой ролью, а не за общим токеном.
APP_OPERATOR_TOKEN даёт доступ ко всем эндпоинтам /v1/admin, включая POST /v1/admin/payments/{paymentID}/refunds и POST /v1/admin/customers/{customerID}/wallet-adjustments. На этом префиксе нет разграничения по возможностям и нет ролей. Передав токен третьей стороне, вы передаёте ей всю поверхность целиком. Считайте его учётными данными уровня установки и меняйте его правкой окружения с последующим перезапуском API.
Каждый изменяющий состояние запрос на этом префиксе — всё, кроме GET, HEAD и OPTIONS, — после обработки записывается в журнал аудита с типом актора operator и идентификатором operator-token, действием admin_api.<метод> (например, admin_api.post), категорией system, исходом (failure для любого 4xx или 5xx), а также шаблоном маршрута и статусом в метаданных. Токен не называет человека, поэтому журнал не может сказать кто; но он говорит, что изменение сделал общий токен, а не оператор в панели, и по этому актору можно фильтровать в браузере аудита. Чтения и запросы, отклонённые из-за неверного токена, не записываются. Ничто внутри этого репозитория не вызывает /v1/admin: Telegram-бот работает с PostgreSQL и Remnawave внутри процесса. Этот префикс существует исключительно как поверхность для внешних интеграций и автоматизации.

Аутентификация

Задайте APP_OPERATOR_TOKEN длинным случайным секретом и передавайте его в каждом запросе:
Сравнение сначала проверяет длину, а затем выполняется за постоянное время; пустой токен в конфигурации отклоняет всё. Отсутствующий или неверный токен — 401 unauthorized. API требует APP_OPERATOR_TOKEN, чтобы вообще собрать свой коммерческий рантайм, наряду с 32-байтным APP_DATA_ENCRYPTION_KEY, APP_REMNAWAVE_URL, APP_REMNAWAVE_TOKEN и APP_VALKEY_URL. Он не является необязательным даже для установки, которая никогда не обращается к этому префиксу, — см. Конфигурацию. Поскольку сессии нет, некоторые обработчики принимают действующего оператора как данные, которые сервер не может проверить: operatorId в теле PUT /v1/admin/maintenance (400 missing_operator, если он отсутствует) и operatorID в решениях по ручным платежам и корректировках кошелька. Эти значения попадают в запись. Передавайте что-то осмысленное и не считайте их аутентификацией.

Правила запросов

  • Content-Type: application/json в любом запросе с телом.
  • Тела декодируются с отклонением неизвестных полей и должны содержать ровно одно значение JSON. Лишнее поле «на будущее» или данные после значения дают 400 invalid_request.
  • Размер тела ограничен 1 МиБ.
  • Обработчики /v1/admin декодируют данные в структуры Go без JSON-тегов, поэтому имена полей сопоставляются без учёта регистра: customerID, customerId и CustomerID работают одинаково. Схемы OpenAPI называют их в camelCase; принимаются оба варианта.
  • Ко всем маршрутам применяется серверный таймаут обработчика в 60 секунд.

Ключи идемпотентности

Каждое изменение, порождающее движение денег, выдачу доступа или проводку в книге, принимает заголовок запроса Idempotency-Key. Это операции, в которых дважды отправленная форма, повтор запроса прокси или переотправленный вебхук не должны породить два заказа, два возврата, два зачисления в кошелёк или двух пользователей Remnawave. Генерируйте одну уникальную строку на одно логическое действие и повторно используйте её дословно при каждой повторной попытке этого действия. Ключ пробрасывается в доменный слой и именно он делает нижележащий SQL идемпотентным; общего кэша повторов на уровне middleware нет, поэтому ключ, переиспользованный для двух разных действий, — ошибка вызывающей стороны. Валидация — это исключительно проверка длины: от 8 до 128 символов, без правил на набор символов. Отсутствующий или выходящий за диапазон ключ на /v1/admin — 400 missing_idempotency_key.
Контракт помечает Idempotency-Key как обязательный для POST /v1/admin/payments/{paymentID}/reconcile, но обработчик его никогда не читает. Сверка идемпотентна сама по себе — она заново опрашивает провайдера через платёжный сервис. Отправка заголовка безвредна.
Остальные префиксы ведут себя иначе, и это важно, если вы обращаетесь и к ним: /v1/account отвечает 400 idempotency_key_required (кроме POST /v1/account/shop/purchase, который отвечает 422), а /v1/panel отвечает 422 idempotency_key_required и принимает любую непустую строку без проверки длины. Вебхуки провайдеров дедуплицируются по собственному идентификатору события провайдера, а не по этому заголовку.

Курсорная постраничная выборка

Списки, которые могут расти неограниченно, возвращают {"items":[…], "nextCursor":"…"} и принимают ?cursor=<value>&pageSize=<n>. Пустой nextCursor означает последнюю страницу. Это постраничная выборка по ключу (keyset) по (timestamp, id), а не через OFFSET: кодирование ключа сортировки удерживает границу страницы стабильной, пока вставляются новые строки, чего смещение обещать не может — иначе интеграция, обходящая журнал аудита во время поступления новых событий, видела бы строки дважды или пропускала их. Запросите первую страницу без cursor, передавайте возвращённый nextCursor обратно дословно и остановитесь, когда он пуст. Считайте значение непрозрачным. В кодовой базе оно кодируется тремя разными способами — base64url, открытый текст и пара параметров cursor + cursorId — и возврат значения без изменений корректен во всех случаях, а его разбор — нет. Некоторые списки /v1/admin вообще не используют курсоры и принимают обычный limit:

Ответы об ошибках

Каждый отказ на всех трёх префиксах формируется одной функцией и возвращается в формате RFC 9457:
Content-Type — application/problem+json. Ветвитесь по последнему сегменту пути в type — это стабильный машиночитаемый код. По одному только статусу нельзя отличить «промокод истёк» от «промокод опускает цену ниже себестоимости», а detail — это проза, которая меняется, когда кто-то улучшает формулировку. title — всегда текст HTTP-статуса, никогда не собственный заголовок. request_id записан в snake_case, тогда как все остальные поля API — в camelCase. Так пишет код и так объявляет схема. Коды, которые стоит обрабатывать явно: Одно и то же нижележащее условие может приходить с разными кодами на разных префиксах: недоступность Remnawave при получении списка устройств — это 502 remnawave_unavailable на /v1/panel и 503 upstream_unavailable на /v1/account. Инструкция, которая трактует «Remnawave лежит» как одно условие, должна знать оба кода. Два вида отказов полностью выпадают из формата problem: коллектор телеметрии отвечает обычным текстом, а GET /v1/panel/audit/export отдаёт CSV потоком уже после того, как записан 200, поэтому сбой в середине потока обрезает файл, а не порождает тело с описанием проблемы.

Идентификаторы запросов

Каждому запросу присваивается идентификатор, и каждый ответ несёт его в X-Request-ID. То же значение появляется как request_id в телах ошибок, как requestId в строках аудита, в идентификаторах корреляции выдачи доступа и в событиях безопасности клиента. Считывайте его из каждого ответа и записывайте в лог. Это единственная строка, которая связывает «клиент говорит, что платёж не прошёл» со строкой лога обработчика, строкой аудита, операцией выдачи доступа и вызовом Remnawave, — и при этом никому не нужно передавать идентификатор клиента по каналу поддержки. На /v1/panel идентификатор запроса ещё и служит единицей дедупликации для дважды отправленной операторской формы: операция над подпиской выводит свой ключ идемпотентности как panel:<subscriptionID>:<operation>:<requestID>.

Ограничения частоты запросов

Ограничения реализованы как счётчики с фиксированным окном в Valkey. К /v1/admin относится только одно из них: Больше ничто на /v1/admin не ограничивается по частоте. Заголовка Retry-After нет, как нет и заголовков с состоянием лимита в ответах, поэтому клиент, упёршийся в ограничение, должен выдерживать паузы по собственному расписанию. Ограничения срабатывают на отказ. Если Valkey недоступен, ограничитель возвращает ошибку и запрос отклоняется, потому что сбой не должен молча снимать защиту. На пути промокода это 503 rate_limit_unavailable; на путях входа оператора это означает, что вход полностью заблокирован, пока Valkey лежит. Адрес клиента, по которому считается ограничение, берётся из разрешения доверенных прокси, описанного в Безопасности. При пустом APP_TRUSTED_PROXIES заголовок X-Forwarded-For игнорируется и используется адрес сокета — а значит, развёртывание за прокси, где эта переменная забыта, ограничивает всех вызывающих как одного субъекта.

Заголовок X-Operator-Reason

Этот заголовок принадлежит /v1/panel, а не /v1/admin. Он читается в каждом запросе /v1/panel и несёт причину, по которой оператор вносит изменение: причина едет в заголовке, а не в каждом теле, потому что она нужна почти каждому изменяющему запросу здесь, и протаскивание её через шестьдесят структур запросов гарантированно привело бы к тому, что в одной из них о ней забудут. Значение попадает в журнал аудита, а для некоторых записей — и в саму запись. Отправляйте X-Operator-Reason: <1..400 символов> в тех изменяющих запросах /v1/panel, которые её требуют: предпросмотр массовой операции, отзыв подарка, изменение статуса и жизненного цикла клиента, регистрация возврата, завершение или отмена выдачи товара, ручное переключение режима обслуживания, решения по блок-листу и апелляции, разбор аномалий и добавление клиента в белый список рисков. Отсутствующая причина возвращается как 422 invalid_request с деталью «Those details are not valid, or a reason is required.» Обработчики /v1/admin этот заголовок игнорируют. Там, где им нужно обоснование, они принимают его в теле — reason в POST /v1/admin/customers/{customerID}/lifecycle, в POST /v1/admin/orders/{orderID}/cancel и в PUT /v1/admin/maintenance.

Разобранный пример

Создадим заказ для существующего клиента и запустим по нему платёж.
Отправьте клиента на checkoutUrl. Вебхук провайдера завершает платёж на POST /v1/payments/webhooks/{provider}; больше от интеграции ничего не требуется. Опрашивайте GET /v1/admin/orders/{orderID}, если вам нужно наблюдать за результатом, а конечный автомат описан в Заказах и платежах. Повтор любого из этих вызовов с тем же Idempotency-Key возвращает запись, созданную первой попыткой, а не создаёт вторую. Повтор с новым ключом создаёт второй заказ.

Что покрывает /v1/admin

Тридцать эндпоинтов, сгруппированных по назначению. Что они намеренно не возвращают:
  • GET /v1/admin/webhooks никогда не отдаёт исходное тело от провайдера. Это собственная полезная нагрузка провайдера, и в ней могут быть платёжные данные; для ответа на вопрос «пришло ли это и было ли принято» достаточно статуса и результата проверки подписи.
  • GET /v1/admin/jobs возвращает сводку аргументов задачи, а не сами аргументы, и прогоняет последнюю ошибку через маскирование чувствительных данных, поэтому ссылка на подписку или bearer-токен из сообщения вышестоящей системы никогда не покидает процесс.
  • Эндпоинт жизненного цикла применяет изменение на уровне изоляции Serializable и соблюдает конечный автомат клиента: suspend — только из active, restore — только из suspended, anonymize — только из deleted и только после того, как истёк срок хранения.
Три оговорки про группу эксплуатации:
  • GET /v1/admin/jobs и маршруты retry/cancel отвечают 503 jobs_unavailable в процессе, к которому не подключён клиент очереди задач. Фильтр state по умолчанию — dead_letter; остальные принимаемые значения — retryable, running, available, scheduled и completed.
  • Долговременное хранилище задач — River, чья схема поставляется вместе с миграциями Atlas, поэтому на стандартной установке эти эндпоинты читают собственные строки очереди River. Представление /v1/panel/system/jobs, показывающее операции выдачи доступа, — другое: оно опирается на собственные таблицы Omniflow.
  • GET /v1/admin/outbox сообщает количество неопубликованных доменных событий и возраст самого старого. Ничто в текущей сборке никогда не помечает строку outbox опубликованной, поэтому это число растёт и не является полезным сигналом о накопившемся отставании. См. Наблюдаемость.

Эндпоинты, не требующие учётных данных

Пять видов маршрутов отвечают без каких-либо учётных данных, и каждый защищён чем-то помимо аутентификации:
  • /healthz, /livez, /readyz — пробы. /readyz публикует стабильную классификацию (timeout, unavailable), а не сообщение драйвера, поэтому не может раскрыть строку подключения.
  • /metrics — отдаётся только когда APP_METRICS_ENABLED включён, и предполагается, что доступ к нему ограничен на обратном прокси. Все метки ограничены по мощности: используется шаблон маршрута, а не конкретный путь с идентификаторами.
  • GET /v1/catalog/plans?locale=ru|en — анонимное чтение витрины. Любая локаль, кроме en, откатывается к ru.
  • POST /v1/payments/webhooks/{provider} — не аутентифицирован по необходимости и защищён проверкой подписи провайдера. Неудачная проверка всё равно записывается и отвечает 400 webhook_rejected; тело больше 1 МиБ — 413 payload_too_large; повторное событие провайдера разрешается в {"status":"duplicate"}.
  • POST /v1/telemetry/events — присутствует, только когда APP_TELEMETRY_COLLECTOR_ENABLED включён (по умолчанию выключен). Это приёмная сторона, которую разворачивает установка-коллектор, а не то, что открывает наружу обычное развёртывание.

Контракт OpenAPI

api/openapi.yaml — исходный контракт (OpenAPI 3.0.3), именно он рендерится в публикуемом справочнике API. Из него генерируются и серверные типы Go, и TypeScript-биндинги, а CI падает, если изменение контракта закоммичено без их перегенерации. Учитывайте эти пробелы, прежде чем генерировать по нему клиент:
  • В нём описаны 113 из 195 маршрутов /v1/panel. Целые области отсутствуют: настройки установки, реестры AI и MCP, служба поддержки, маркетинг, лояльность, OIDC для операторов, представления состояния здоровья и режима обслуживания. Поверхности /v1/admin и /v1/account описаны полностью.
  • Ни одна операция не ссылается на схемы безопасности panelSession или accountSession. Схемы определены, а корневого security нет, поэтому в терминах OpenAPI каждая операция /v1/panel и /v1/account читается как неаутентифицированная. Только 30 операций /v1/admin несут security: [{operatorToken: []}]. Сгенерированный клиент не будет знать, что вызовам панели нужны учётные данные.
  • Требуемые права доступа указаны только в текстовых описаниях, поэтому никакой инструментарий не может их извлечь. Авторитетный ответ — это requirePermission(...) в точке монтирования каждого маршрута.
  • Ответы не валидируются по контракту во время выполнения. Обработчики декодируют данные в анонимные структуры и пишут вручную собранные map; сгенерированные серверные типы Go не импортируются нигде. Ответ может разойтись со своей схемой без падения тестов.
  • info.version содержит 1.0.0. Он отражает линию выпусков, а не отдельную версию контракта, поэтому по нему нельзя понять, какие из перечисленных выше пробелов уже закрыты.
  • Две операции задокументированы безусловно, но в коде условны: GET /metrics и POST /v1/telemetry/events. В более общем случае маршрут /v1/panel или /v1/account, чей сервис не подключён, отсутствует (404), а не отдаёт 503 — 503 сообщил бы анонимному вызывающему, что именно настроил оператор.

Связанные страницы

Конфигурация

APP_OPERATOR_TOKEN, APP_TRUSTED_PROXIES и остальное окружение.

Безопасность

Поверхности с учётными данными, что никогда не пишется в логи и как запечатываются секреты.

Заказы и платежи

Конечный автомат заказа, который приводит в движение разобранный пример.

Задачи и очереди

Что показывают эндпоинты задач и что на самом деле повторяется.