INOTrackбыстрота, надежность и ахуенность

Публичный API и MCP-сервер

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

Два независимых механизма:

  1. Bearer API-токен (Authorization: Bearer <token>) — для всего /api/v1/*.
  2. Сессионная кука админки (it_session) — единственный способ вызвать эндпоинты токенов (GET/POST /api/v1/tokens, DELETE /api/v1/tokens/{id}), потому что для выпуска первого токена ещё нет самого токена для авторизации.

Выпуск токена

# 1. логин, кука сохраняется в cookies.txt
curl -s -c cookies.txt -X POST https://<трекер>/api/internal/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"<пароль>"}'

# 2. выпуск токена под этой сессией
curl -s -b cookies.txt -X POST https://<трекер>/api/v1/tokens \
  -H 'Content-Type: application/json' \
  -d '{"name":"my-integration","scopes":["read","write"],"rate_limit":600}'

Значение поля token в ответе отдаётся один раз — дальше в базе хранится только его хэш. Токен можно привязать к сроку действия (expires_at) и своему rate-limit (запросов в минуту, дефолт — 600). Роль admin может выпустить токен от имени другого пользователя (user_id).

Отзыв: DELETE /api/v1/tokens/{id} (тоже под сессионной курой).

Права (scopes)

  • read — требуется на все GET.
  • write — требуется на все остальные методы (POST/PATCH/DELETE).
  • mcp — отдельный scope для токенов, которые вы выпускаете под MCP-сервер (см. ниже); сам по себе доступ не расширяет — используйте вместе с read/write. Обязателен, если в лицензии есть модуль MCP, но нет публичного API: тогда /api/v1/* принимает только токены с mcp.
  • push_devices, push_send, app_events — узкие scope для бэкенда витрин (устройства и отправка пушей, события приложений).
  • clicks — узкий scope только для серверного клика POST /api/v1/clicks (витрины). Не даёт ни read, ни write. Выпустить могут роли admin и buyer, write — только admin.

Токену не хватает нужного scope → 403 forbidden.

Rate-limit

Считается на токен, в запросах в минуту (rate_limit при выпуске, дефолт 600). Превышение → 429 rate_limited. Токен без явно заданного лимита ограничений не имеет.

Формат ошибок

Единый для всего API:

{"error": "unauthorized", "message": "нужен заголовок Authorization: Bearer <токен>"}

Машинные коды: unauthorized (нет/неверный токен) · forbidden (не хватает scope) · license_required (фича выключена текущей лицензией, 403) · not_found · bad_request · validation (400, неверные параметры списка: limit, sort, ids, cursor) · conflict и in_use (409) · module_unavailable (модуль недоступен в этой установке, 503) · unavailable (503, временно недоступна зависимость, повторите) · cache_unavailable (502, изменение потока не удалось опубликовать в кэш клик-пути) · clickhouse (502) · rate_limited (429, вместе с заголовком Retry-After) · unsupported_media_type (415, у изменяющего запроса тело без Content-Type: application/json; исключения — загрузки файлов и CSV-импорт, у них свой тип) · cross_origin (403, межсайтовый изменяющий запрос по куке сессии) · payload_too_large (413, тело больше потолка: INOTRACK_API_MAX_JSON_BYTES, по умолчанию 8 МиБ) · internal. У отдельных ручек есть свои коды (offer_capped, in_progress, app_not_found…): они перечислены в описании ответов операции в спецификации OpenAPI. В спецификации этот формат — схема components/schemas/Error, его описывает ответ default каждой операции.

Списки: пагинация, поиск, сортировка

Правила одни для всех списков каталога (/offers, /flows, /sources, /sites, /advertisers, /webhooks, /webhooks/{id}/deliveries, /manual-costs и вложенных списков источника и сайта):

Параметр Что делает
limit Размер страницы. По умолчанию 50, всё больше 500 срезается до 500. Ноль, отрицательное число или не число дают 400 validation
offset Сдвиг от начала выборки
q Поиск по имени (регистр не важен). Число ищется ещё и как id. Строка короче двух символов игнорируется, если это не число
sort field,-field, до трёх полей. Минус означает обратный порядок. Допустимые поля свои у каждого списка, неизвестное поле даёт 400 validation. Равные значения всегда упорядочиваются по id, поэтому порядок страниц стабилен
ids 1,2,3: только перечисленные записи (не больше 500)
facets Например status,owner: счётчики по значениям во всей выборке. Счётчик поля считается без фильтра по этому же полю

Ответ имеет форму {"items": [...], "total": 123, "limit": 50, "offset": 0}. items всегда массив. total равен null, если общее число не удалось посчитать за отведённое время. Тот же total приходит в заголовке X-Total-Count. Старые клиенты без параметров получают, как раньше, первую страницу прежнего размера в items/total: 50 записей у каталога и доставок вебхука, 500 у /webhooks, /manual-costs (потолок limit у него 1000), /verticals и вложенных списков источника и сайта. Новые поля в ответе только добавились.

Журналы, которые растут без ограничений, листаются курсором: cursor вместо offset, в ответе next_cursor (null на последней странице). Курсор запоминает фильтры, q и sort, при которых получен. Если передать его с другими параметрами, придёт 400. offset и cursor вместе тоже дают 400.

Основные сущности

CRUD (список/получить/создать/изменить/удалить, где применимо) для:

  • /api/v1/advertisers — рекламодатели/CPA-сети;
  • /api/v1/offers, плюс POST /offers/{id}/pause и /resume;
  • /api/v1/flows, плюс PATCH /flows/{id}/weights (веса сплита офферов), POST /flows/{id}/pause и /resume;
  • /api/v1/sources — источники трафика;
  • /api/v1/sites — сайты сетки;
  • /api/v1/tokens — управление собственными API-токенами;
  • /api/v1/webhooks — подписки на вебхуки, плюс /webhooks/{id}/deliveries (лог доставок).
  • POST /api/v1/clicks — серверный клик без редиректа для витрин (scope clicks или write, идемпотентность по Idempotency-Key, клик пишется с host = "s2s"); сайт определяется в том же порядке, что на /go: явный site_id → sub1 == sites.sub_value → хост Referer (sites.domain / site_domains) → единственный сайт потока (при нуле или нескольких привязках сайт не угадывается) (подробности — docs/showcases.md). Необязательный allow_fallback: true включает цепочку запасной поток/смартлинк → fallback_offer → default_url; allow_last_resort: true разрешает оффер на автопаузе после этой цепочки (если разрешён инстансом). Без флагов сохраняется 409 без записи клика. Успех возвращает selected_by: flow|fallback_flow|fallback_offer|default_url|last_resort; клик записывается на каждом успешном пути. При fallback_flow/default_url значение offer_id=0: запасной поток выберет конечный оффер сам, постбек сети по исходному clickid не придёт. fallback_flow ведёт на трекер, default_url — на заданный адрес. Уникальность определяется окном потока по visitor_id (до 128 символов), иначе по IP+UA; без этих данных клик уникален. is_repeat: true означает повтор. При unique_block_repeat повтор может уйти только в запасной путь с allow_fallback, иначе 409 unique_repeat.

bandit_goal задаёт цель бандита: approve по умолчанию, любая известная цель (ftd, reg, dep, rebill, lead, reject) или revenue. Для цели считаются её конверсии и выплаты; для revenue — конверсии с payout_usd > 0 и сумма выплат всех целей. Тестовые и вручную исключённые конверсии не учитываются. Поле доступно в POST/PATCH и ответах потоков.

У потока три режима ротации (rotation_mode): fixed — сплит по весам офферов, bandit — по весам, посчитанным ротатором (bandit_weight), и priority — каскад: у каждого оффера потока есть priority (чем больше число, тем раньше он в каскаде), и клик идёт в доступный оффер самого высокого приоритета; при равных приоритетах — обычный сплит по весам внутри уровня. Если на верхнем уровне доступных офферов не осталось, трекер сначала проверяет fallback_offer_id каждого оффера этого уровня (если он задан и ведёт на другой оффер того же потока) и только потом переходит на следующий уровень приоритета.

Условия выплаты по ГЕО доступны в geo_payouts в списке и карточке оффера (всегда массив, без условий — []). POST и PATCH принимают этот массив как полную замену: отсутствие поля сохраняет набор, [] очищает его. До 250 строк, одна на ISO-3166 alpha-2 страну; принадлежность страны к offers.geo не требуется. Клонирование копирует условия.

Выбирается строка по ГЕО клика, иначе общие поля offers.*; отдельные поля строки не наследуются. Пример: {"geo":"AZ","payout_model":"hybrid","payout":50,"revshare_percent":25,"currency":"USD","goal":"ftd","note":""}. Гибрид — фиксированная часть за goal плюс RevShare. revshare_percent справочный: трекер сам не считает процент от депозита, доход RevShare приходит постбеками rebill/commission. При записи пустая модель становится cpa, валюта — валютой оффера, цель — cap_event (иначе approve). Для revshare фиксированная часть обнуляется, для cpa/cpl обнуляется процент. Сумма неотрицательна, процент от 0 до 100. Правки отражаются в аудите оффера; payout_history хранит только историю общей ставки.

В GET /offers и GET /offers/{id} у оффера есть поля для витрин: network (код сети рекламодателя), network_offer_id (id в сети, равен external_id) и cap_state (состояние кап, null при недоступном Redis; по каждому окну — метрики conversions/clicks/payout: детали — в разделе «Капы» ниже). Для витрин же предназначены маркировка (erid, реквизиты рекламодателя, ?has_erid=, ?updated_since=, POST /offers/erid-import) и доп. поля (custom_fields, источник custom_fields_source). Набор полей задаётся определениями инстанса; ответ содержит подходящие вертикали поля и все заполненные. conditions и conditions_source — deprecated-алиасы для старых витрин: conditions по-прежнему содержит все активные ключи, отсутствующие значения — null. POST/PATCH принимает одно из custom_fields или conditions: частичный патч, null удаляет ключ. Обязательность проверяется только при ручной записи через API, не при импорте сетей и CSV erid.

/api/v1/offer-fields: GET — список (включая архив), POST — создание; PATCH /api/v1/offer-fields/{key} — правка/восстановление, DELETE /api/v1/offer-fields/{key} — архив (значения сохраняются), ?purge=1 удаляет только неиспользуемое определение и снимает ссылки pair_max на него. Запись доступна только admin. GET /api/v1/offer-fields/presets показывает встроенные пресеты. Включить «Финпродукты» на rivo: POST /api/v1/offer-fields/presets/fin_showcase/apply с admin-токеном со scope write, без тела. Повторный вызов безопасен: ручные правки сохраняются.

Массовые операции: POST /flows/batch (до 200 потоков, каждый поток — своя транзакция, в ответе итог по каждому id) и POST /offers/batch, /sources/batch, /landings/batch (до 500 записей, весь пакет — одна транзакция: чужая или несуществующая запись откатывает всё). Действия каталога: pause, resume, archive, set_tags, у офферов ещё set_vertical, set_payout, set_caps. dry_run: true возвращает, что изменится, ничего не меняя. Потоки, в конфиге которых лежат изменённые офферы, перепубликуются один раз на пакет. Теги — поле tags оффера и источника, фильтр списков ?tag=.

Полная схема тел запросов и ответов — спецификация OpenAPI, см. следующий раздел.

Продукты (/api/v1/products)

Продукт — карточка витрины (Белки), под которой сходятся офферы РАЗНЫХ сетей одного и того же предложения: витрина показывает одну карточку («Займер, займ до 30 000»), а за ней могут стоять офферы нескольких CPA-сетей и прямой договор. Справочник продуктов — /api/v1/products (список/карточка/создание/правка/архивация, как /verticals): чтение — любой авторизованный, запись — только роль admin.

Привязка оффера к продукту — обычный PATCH /offers/{id} с полем product_id (существующий, не в архиве; null — отвязать). Сама склейка никогда не происходит автоматически: ни создание продукта, ни правка, ни синк каталога сети не трогают offers.product_id.

Подсказки склейки (GET /products/suggestions) — автоматическое предложение, не привязка: трекер пересчитывает их после правки brand/inn/vertical/archived продукта и при каждом синке каталога сети, по двум правилам (приоритет — по порядку):

  1. ИНН рекламодателя оффера (advertiser_inn) совпал с ИНН продукта (inn);
  2. бренд продукта (brand) входит в название оффера без учёта регистра.

Кандидат отбрасывается, если у оффера и продукта заданы разные непустые вертикали. Подсказка живёт до решения человека: подтвердить (PATCH /offers/{id} с product_id, сама подсказка после этого снимается) или скрыть (DELETE /products/suggestions/{offer_id}) — скрытая подсказка не возвращается следующим синком.

GET /products/{id}/offers отдаёт офферы продукта тем же форматом, что GET /offers (сети, cap_state, счётчики потоков), с учётом видимости вызывающего.

Условия выбора оффера

В offers[] потока поле targeting задаёт условия конкретного оффера. Например:

{"offer_id": 42, "weight": 100, "is_active": true, "targeting": [
  {"field": "city", "op": "in", "values": ["Moscow", "Saint Petersburg"]},
  {"field": "sub3", "op": "eq", "values": ["telegram"]},
  {"field": "site", "op": "eq", "values": ["12"]}
]}
  • field: region, city, sub1–sub10, utm_source, utm_medium, utm_campaign, utm_term, utm_content, site.
  • op: eq — равно первому значению, ne — не равно первому значению, in — входит в список, nin — не входит в список. Строки сравниваются без учёта регистра.
  • Все правила оффера соединяются И; пустой массив означает отсутствие условий. Город — как в отчёте по городам; регион — ISO-код или название в зависимости от базы GeoIP. Пустой город/регион не отсекает оффер при любом операторе. Отсутствующая метка sub/utm или сайт (site_id = 0) не проходит eq/in, но проходит ne/nin.
  • site сравнивается численно с sites.id; в values передавайте положительные десятичные ID строками, например ["12"] (API не проверяет числовой формат этих строк).

Условия задаются через POST /api/v1/flows и PUT /api/v1/flows/{id}/offers; в PATCH /api/v1/flows/{id}/weights их нет. Передавайте полный желаемый список условий: отсутствующее targeting в заменяемом оффере снимает его условия. Панель позволяет редактировать правила у каждого оффера, для site — выбирать сайты. API принимает до 10 правил на оффер, от 1 до 20 значений на правило, до 255 символов в значении; пробелы по краям значений обрезаются, пустые значения и неизвестные поля/операторы возвращают 400.

После публикации потока условия проверяются до взвешенного выбора и при выборе оффера «последнего шанса», вместе с остальными ограничениями. На /go/{token} сигналы sub/utm берутся из query-параметров, сайт определяется по порядку: явный site_id, sub1 == sites.sub_value, хост Referer (sites.domain / site_domains), затем единственный сайт потока. При нуле или нескольких привязках сайт не угадывается. Таргетинг офферов получает итоговый сайт; guard вызывается до резолва потока и fallback из привязки не видит. В POST /api/v1/clicks при выборе из потока передавайте sub1–sub10, utm_source, utm_medium, utm_campaign, utm_term, utm_content и числовой site_id в JSON-теле; sub/utm — до 255 символов, site_id — неотрицательное целое, 0 означает отсутствие сайта. Город и регион в обоих путях определяет GeoIP по IP клика. Старые edge-ноды игнорируют новое поле конфигурации и условий не применяют: обновите их перед использованием таргетинга.

Спецификация OpenAPI 3.1

Машиночитаемое описание всего /api/v1/* — OpenAPI 3.1.0 (схемы — JSON Schema 2020-12):

  • GET /api/v1/openapi.json — JSON, для генераторов клиентов, Postman/Insomnia и MCP;
  • GET /api/v1/openapi.yaml — то же самое в YAML.

В спецификации есть все маршруты /api/v1 (соответствие спецификации и реальных маршрутов проверяется автотестами в обе стороны), схемы тел и ответов, operationId у каждой операции и единая схема ошибки Error. Внутренних ручек (/api/internal/*) в ней нет. Прокси модулей (/reports/*, /caps/*, /rules/*, /postback/*, /finance/*, /integrations/*) описаны одной операцией на метод без схем — маршруты модулей перечислены ниже в этом документе. Часть маршрутов модулей описана в спецификации отдельно, со схемами и кодами ошибок: сырые клики (/reports/clicks, /reports/clicks.csv, /reports/clicks/settings), шаблоны и предпросмотр правил (/rules/rule-presets, /rules/rules/preview) и блеклист зон (/blacklist); новые маршруты модулей попадают в спецификацию так же. Ошибки модуля правил приходят как {"error": "<текст>"} без message (схема RulesError).

# скачать спецификацию
curl -s https://<трекер>/api/v1/openapi.json -o inotrack-openapi.json

# список операций: метод, путь, operationId
jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
       | select(.key != "parameters") | "\(.key | ascii_upcase) \($p) \(.value.operationId)"' inotrack-openapi.json

# схема оффера
jq '.components.schemas.Offer' inotrack-openapi.json

# клиент, например на TypeScript (https://openapi-generator.tech; генератор должен понимать OpenAPI 3.1)
npx @openapitools/openapi-generator-cli generate -i inotrack-openapi.json -g typescript-fetch -o ./inotrack-client

servers в спецификации — относительный /api/v1: генератору клиента или Postman укажите базовый адрес https://<трекер>/api/v1. Bearer-токен — схема bearerAuth; /tokens работают только под сессионной кукой (sessionCookie).

Обе ручки отвечают без токена и сессии — авторизация на этих двух точных путях не проверяется. Остальной /api/v1 по-прежнему требует авторизацию: без Authorization придёт 401 unauthorized.

Отчёты, капы, правила, постбеки, финансы, интеграции — через прокси

Эти шесть модулей не имеют собственных путей в /api/v1, а проксируются целиком: /api/v1/reports/*, /api/v1/caps/*, /api/v1/rules/*, /api/v1/postback/*, /api/v1/finance/*, /api/v1/integrations/*. Если модуль недоступен в вашей установке — соответствующий префикс отвечает 503 module_unavailable.

⚠️ /api/v1/integrations/* сегодня отвечает 503 всегда, и это осознанное решение, не баг: интеграции рекламных площадок управляются из админки под сессионной кукой (/integrations, не под /api/v1), публичного API для них пока нет.

Прокси сам определяет нужный scope по HTTP-методу (GET → read, остальное → write), кроме путей модуля отчётов, явно помеченных как read-only (например, экспорт CSV) — им достаточно read, хотя метод у них POST.

Из финансового модуля стоит отдельно знать GET /api/v1/finance/fx/status (только роль admin) — свежесть источника курсов валют (дата последнего известного курса ЦБ РФ/CoinGecko, возраст, флаг stale), см. FAQ про то, как курс фиксируется на дату события.

Отчёты (/api/v1/reports/*)

Доступные маршруты:

Путь Метод Назначение
/query POST универсальный отчёт: {dimensions, metrics, filters, date_from, date_to, timezone, currency, ...}
/dims, /metrics GET список доступных измерений/метрик
/seo GET отчёт «сайт → страница → доход»
/export.csv POST потоковый CSV-экспорт
/drilldown POST провал в детали (кампания → адсет → крео → площадка)
/clicks POST сырые клики по условиям строки отчёта: {filters, date_from, date_to, timezone, include_bots, status, limit, cursor} → {items, next_cursor, labels, meta}; курсорная пагинация (cursor = next_cursor прошлой страницы), до 500 кликов на страницу, таймаут 40 с (504 — сузьте период)
/clicks.csv POST те же клики CSV-файлом, потоково, до 100 000 строк (больше — 422 export_too_large); не больше двух выгрузок сразу — иначе 429 с Retry-After
/clicks/settings GET/PUT режим показа IP в сырых кликах: prefix (подсеть и хэш, по умолчанию), hash, hidden; менять — только admin
/cohorts POST когортный анализ и LTV
/plans, /plans/{id}, /plans/fact POST/DELETE/GET план-факт и линейный прогноз
/shares, /shares/{id}/revoke POST/POST управление секретными read-only ссылками на отчёт — требуют Bearer-токен, как и остальной прокси
/shares/view/{token} GET публичный просмотр по ссылке — не под /api/v1, открывается без авторизации (см. FAQ)
/annotations, /annotations/{id} GET/POST/PATCH/DELETE ручные и авто-аннотации на графиках
/creatives, /creatives/{id}, /creatives/{id}/upload, /creatives/report GET/POST/PATCH/DELETE библиотека креативов, метрики по креативу
/sql POST SQL-песочница, см. ниже

Измерение brand_advertiser_id («Рекламодатель (бренд)») группирует клики и конверсии по конечному рекламодателю оффера. Привязка читается из текущего offers.brand_advertiser_id в Postgres, поэтому её изменение пересчитывает и прошлые события. Оффер без бренда даёт 0 («Без рекламодателя»). Фильтр выбирает сущность рекламодателя; подписи id приходят в labels. Расход по бренду недоступен. Измерения advertiser_id и network сохраняют прежний смысл сети.

⚠️ include_bots и P&L. /query, /drilldown, /cohorts по умолчанию (include_bots не передан или false) считают выручку и остальные денежные метрики без конверсий с is_bot_click = 1 — это показатель "сколько заработали на людях", который видит пользователь в отчётах. Финансовый P&L (/api/v1/finance/*) считает выручку по всем approved-конверсиям независимо от is_bot_click — это полная сумма для бухгалтерии и сверки с сетью, ей всё равно, кто нажал кнопку. Из-за этого выручка отчёта с дефолтным include_bots=false и выручка P&L за один и тот же период не обязаны совпадать — расхождение ровно на сумму approved-конверсий ботового трафика. Чтобы сравнить отчёт с P&L "яблоки к яблокам", запрашивайте отчёт с include_bots=true.

SQL-песочница (POST /api/v1/reports/sql)

Произвольный SELECT (в т.ч. WITH ... SELECT) к ClickHouse напрямую — только для роли admin. Защита:

  • только один оператор SELECT/WITH за вызов (лишняя ; внутри текста отклоняется);
  • запрос выполняется отдельным пользователем ClickHouse с правом только SELECT на базу — INSERT/DDL/DROP физически невозможны на уровне прав, не только по фильтру текста;
  • жёсткий таймаут выполнения — 10 секунд;
  • жёсткий лимит строк — 1000 (ClickHouse обрезает результат сам, result_overflow_mode: break), а не отдаёт ошибку при превышении;
  • отдельный маленький пул соединений (до 3) — тяжёлый ad-hoc запрос не отъедает соединения у боевых отчётов;
  • каждый запрос логируется (кто, текст запроса, длительность, число строк, успех/ошибка).
curl -s -X POST https://<трекер>/api/v1/reports/sql \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"query": "SELECT source, count() FROM clicks WHERE ts >= today() - 1 GROUP BY source ORDER BY count() DESC LIMIT 20"}'

Капы (/api/v1/caps/*)

Три метрики капы на оффер, каждая — по своим окнам день/неделя/месяц/total (в таймзоне оффера, cap_timezone): conversions (как раньше, по offers.cap_event — lead/approve/ftd), clicks и payout (сумма выплат в валюте оффера). Любая исчерпанная метрика блокирует оффер по этому окну целиком — перелив на fallback общий для всех метрик.

  • GET /offers/{id}/status — счётчики капы по каждому окну: {window, count, limit, blocked, clicks, clicks_limit, payout, payout_limit, currency, blocked_by} (blocked_by — список метрик, из-за которых заблокировано именно это окно; currency — только если у окна задан лимит по payout), плюс manual_pause, broken, allowed, block_notes и healthcheck (автопауза health-check). То же самое отдаётся полем cap_state в GET /offers и GET /offers/{id} (витринные поля, см. выше), только null, если Redis недоступен.
  • POST /offers/{id}/reset-cap?window=day&metric=clicks — сброс счётчика окна вручную (эксплуатация), только роль admin. window — day (по умолчанию) week/month/total; metric — conversions (по умолчанию) / clicks / payout; что-то другое — 400.

Вебхуки

POST /api/v1/webhooks создаёт подписку на события: conversion, cap.reached, rule.fired, anomaly, offer.down. Секрет подписи (secret) отдаётся один раз в ответе на создание.

Событие cap.reached (капа выбрана) несёт offer_id, advertiser_id, window, metric (conversions/clicks/payout), count/limit (для payout — округлённая до целого сумма и лимит), fallback_offer_id (если задан) и, только для metric=payout, amount/amount_limit (точная сумма и лимит) и currency. Предупреждение о приближении к капе (80% по умолчанию, INOTRACK_CAPS_WARN_PERCENT) вебхуками не рассылается — это внутренний алерт (Telegram, мобильный пуш, лента «Требует внимания», тот же AlertKind cap_reached с severity: warning и отдельным ключом антиспама), у публичного API события для него нет.

Событие offer.down с полем Country сообщает о проблеме в дополнительной стране и не означает остановку оффера. Поле alive_country (с confirmed: true и autopaused: false) означает обратное: оффер не открывается по решающей проверке, но из этой страны гео он открывается, поэтому автопауза не поставлена и трафик продолжается (правило агрегации стран, INOTRACK_CAPS_HEALTHCHECK_EDGE_COUNTRIES).

offer.down с confirmed: true приходит один раз на подтверждённую серию провалов, а не на каждую проверку: без автопаузы (autopause_on_broken выключен) — на первой подтверждённой проверке, с автопаузой — при её постановке, то есть раз в срок паузы (INOTRACK_CAPS_BROKEN_TTL_MIN), если сайт лежит дольше. Серия обрывается, когда страница снова открывается; истечение паузы и смена настройки автопаузы начинают отсчёт заново. Серии проверки central и проверки с edge-ноды страны оффера считаются отдельно.

Очередь доставки — персистентная (переживает рестарт процесса): при ошибке доставки — экспоненциальный бэкофф (5с, 10с, 20с... с потолком в 1 час) до max_attempts (фиксировано 8 попыток, полем API не настраивается), затем статус failed. Каждая попытка видна в GET /api/v1/webhooks/{id}/deliveries (код ответа, ошибка, число попыток).

Подпись тела — заголовок:

X-Inotrack-Signature: sha256=<hex hmac-sha256(secret, тело_запроса)>

Сверяйте константным сравнением (hmac.Equal), а не строковым ==.

MCP-сервер (работа с трекером из Claude Code)

Отдельная программа inotrack-mcp поверх этого же публичного API — своей БД или Redis не трогает, только HTTP до /api/v1. Запускается на вашей машине рядом с Claude Code, а не на сервере трекера. Готовый бинарь для вашей ОС запросите в поддержке.

Переменные окружения: INOTRACK_API_TOKEN (обязательна, рекомендуется отдельный токен со scope mcp+read, и write только если нужны write-инструменты) и INOTRACK_API_URL (дефолт http://localhost:8081/api/v1).

Подключение:

claude mcp add inotrack \
  --env INOTRACK_API_URL=https://<трекер>/api/v1 \
  --env INOTRACK_API_TOKEN=<токен> \
  -- /путь/до/bin/inotrack-mcp

Инструменты чтения

report_query, list_offers, list_sites, list_flows, get_anomalies, get_finance_summary, alerts_feed, raw_clicks, site_consoles, site_guard_rules, list_rank_projects, list_rank_keywords, rank_keyword_history, rank_regions.

  • alerts_feed — лента алертов из колокольчика панели (90 дней): фильтр по типу (kind), только непрочитанные, курсор страниц. Отметка прочитанным — mark_read (id пунктов) или mark_all_read (все, можно с kind и through_id); прочитанность личная, для отметки токену нужен scope write. Баер видит алерты своих потоков и их офферов, как в панели.
  • raw_clicks — сырые клики с фильтрами отчёта (filters, date_from/date_to, статус клика), до 500 на страницу и курсор. Баер — только клики своих потоков. IP — не больше, чем разрешает настройка приватности трекера (подсеть и короткий хэш, только хэш или ничего); аргумент ip_mode может её только ужесточить.
  • site_consoles — статус подключения поисковых консолей сайта (GSC / Яндекс.Вебмастер): connected, property, has_secrets/secret_mask (секреты не читаются), last_sync_at/ last_error, плюс seo_last_import_at и rows_30d сайта.
  • list_rank_projects/list_rank_keywords/rank_keyword_history/rank_regions — проект позиций сайта, его фразы с текущими позициями, история фразы и справочник регионов движка (код региона — Яндекс lr, Google gl).
  • site_guard_rules — правила защиты сайта от скрейпинга (клоаки): условия (UA/CIDR/ASN/ referer/geo/вердикт бот-детектора), действие и приоритет проверки; include_global добавляет глобальные правила. Непустой список (или заглушка/guard_action сайта) закрывает пункт чек-листа «guard».

Инструменты записи

pause_offer, resume_offer, set_weights, blacklist_add, blacklist_remove, bulk_update, site_console_connect, site_console_disconnect, site_console_import, create_rank_project, add_rank_keywords, delete_rank_keywords, site_guard_rule_add, site_guard_rule_remove, site_stub_set. Требуют явного "confirm": true в аргументах — без него сервер не выполняет действие, а возвращает текст с описанием последствий (не ошибка вызова, а осознанный отказ). Повторите вызов с confirm: true, чтобы выполнить. Все write-вызовы идут тем же токеном через публичный API, поэтому запись в audit log делает сам трекер (актёр вида api-token:<имя токена>) — отдельного аудита в MCP-сервере нет и не нужно.

bulk_update — пауза или включение сразу многих офферов, лендов или источников (до 500 id, одна транзакция: меняются все или ни одна). Исключение — ответ cache_unavailable: изменения уже сохранены, но потоки не перепубликованы; повторите тот же вызов с confirm: true. Без confirm: true (или с dry_run: true) трекер прогоняет пакет с откатом, и инструмент показывает, что изменится: сколько записей, какие именно и сколько потоков будет перепубликовано. С confirm: true пакет выполняется, каждая изменённая запись попадает в audit log. Права — как у массовых операций в панели: viewer не может, баер — только свои записи (чужая или командная запись в пакете отменяет весь пакет).

site_console_connect/site_console_disconnect/site_console_import — подключение, отключение и ручной импорт поисковой консоли сайта (GSC / Яндекс.Вебмастер). Секреты (access_token, service_account_json) только записываются — ни этот, ни другой инструмент их не читает обратно; ответ показывает лишь has_secrets и маску.

create_rank_project — создать проект позиций сайта (пустые name/domain подставляются из сайта). add_rank_keywords — пакетное добавление фраз (до 1000 за вызов с учётом поисковиков, каждая фраза заводится в каждом указанном engines; дубли пропускаются — skipped). delete_rank_keywords — пакетное удаление: ровно один из ids/phrases/all: true.

site_guard_rule_add/site_guard_rule_remove — создать или удалить правило защиты сайта (scope/site_id подставляются автоматически); правило без единого условия требует allow_wildcard: true, иначе отказ wildcard_not_confirmed. Действует на клик-пути сразу (кэш обновляется синхронно), а не после фонового тика. site_stub_set — загрузить заглушку сайта и/или включить её (enable: true выставляет guard_action=stub); только роль admin.

Известное ограничение MCP

report_query/get_anomalies (через модуль отчётов) и get_finance_summary (через финансовый модуль) вернут isError: true с текстом о недоступном модуле, если в вашей установке этот модуль недоступен (API отвечает 503 module_unavailable) — это ожидаемое поведение, не ошибка MCP-сервера.

Любой отказ API (401, 403, 404, 409, 415, 5xx) инструмент возвращает как isError: true со статусом и причиной (у 5xx без message — только статус: текст error от модуля не показывается); токен из INOTRACK_API_TOKEN в текст ошибки не попадает, даже если ответ его повторяет.

Дальше

Обслуживание инстанса (бэкапы, обновления, доктор) — Обслуживание инстанса. Честные ответы на вопросы про надёжность — FAQ.

Импорт каталога сети: снятие сетевой паузы

При network_status=active импорт возвращает status=active только существующему офферу на сетевой паузе: network_paused=true либо последняя смена статуса в audit_log — offer.network_paused / offer.network_missing. Последующая ручная смена статуса (включая повторную ручную паузу) запрещает автоматический возврат. Активные ручные блокировки, capped, broken и паузы, поставленные сетью поверх capped, сохраняются. Новые офферы создаются в paused: их включает оператор после проверки маркировки и условий.

Возврат перепубликует потоки и пишет offer.network_reactivated с before/after и причиной «сеть вернула оффер». Отчёт содержит network_reactivated (поле Reactivated) со статусом active; панель показывает «Возвращены сетью: N». Отсутствие erid само по себе не запрещает трафик: такой возврат отмечается «без erid» в отчёте и without_erid=true в аудите. Предпросмотр рассчитывает те же изменения без записи.

Дерево вертикалей

Вертикали образуют дерево до 6 уровней с едиными латинскими кодами: finance → banks → credit_cards. Старые коды и алиасы (мсб → sme, рефинансирование → refinancing) принимаются API и приводятся к канону. Фильтр ?vertical=<узел> у офферов, сайтов и потоков включает сам узел и всех потомков; ?subvertical= у офферов выбирает строго указанный узел. Справочники /verticals и /dictionaries/verticals возвращают parent_code, path (коды от корня) и depth (корень — 1). PATCH /verticals/{code} меняет parent_code и sort; циклы и глубина свыше 6 отклоняются. DELETE требует replace_with для используемого узла; если есть только дети, children=to_parent переносит их к родителю удаляемого узла (у корня — в корни). Доп. поля, привязанные к узлу, действуют во всём поддереве. В потоках vertical хранит корень, subvertical — выбранный узел любой глубины; автоподбор принимает узлы любой глубины и включает их потомков. Формат условий Белки сохраняется.

В универсальном отчёте vertical группирует офферы по корню дерева, subvertical — по второму уровню, а vertical_node — по точному узлу оффера. Подпись vertical_node содержит полный путь, например «Финансы / Банки / Кредитные карты». Фильтры eq/in этих измерений включают выбранные узлы и их потомков; neq/not_in исключают соответствующие поддеревья.

Прогноз выплат в универсальном отчёте

POST /api/v1/reports/query принимает новые ключи в metrics:

Ключ Значение
payout_pending Сумма выплат действующих pending-конверсий в валюте отчёта.
approve_rate Апрув оффера: approved / (approved + rejected) × 100 за последние 30 дней от момента запроса. Без решённых — 100%.
payout_forecast payout + сумма по офферам (payout_pending оффера × его апрув / 100).
profit_forecast payout_forecast - cost.
roi_forecast profit_forecast / cost × 100; при нулевом расходе — null (в панели «—»).

Апрув считается по действующим решениям транзакций (clickid, txid) независимо от дат, измерений и фильтров отчёта. В срезе нескольких офферов колонка approve_rate показывает среднее их ставок, взвешенное числом действующих конверсий среза; прогноз выплаты всегда использует отдельную ставку каждого оффера, а не среднюю. При отсутствии конверсий справочный апрув — 100%. Тестовые конверсии исключены; ручные исключения применяются и к истории апрува (кроме явно запрошенного include_excluded=true, как у payout). Pending после reject/approve не учитывается. Действующая approved-строка попадает в существующую метрику payout. Финансы, P&L и формулы payout/profit/roi не меняются: новые показатели — прогноз, а не полученные деньги.

Пример тела запроса:

{"dimensions":["source_id"],"metrics":["payout","payout_pending","approve_rate","payout_forecast","profit_forecast","roi_forecast"],"date_from":"2026-10-27","date_to":"2026-10-27","currency":"RUB","timezone":"Europe/Moscow"}

Расход из ссылки источника

В POST /api/v1/sources, PATCH /api/v1/sources/{id} и ответах источника доступны:

  • cost_param: имя query-параметра входящего клика с ценой сети, например cost или cpc. По умолчанию пусто (выключено).
  • cost_currency: USD (по умолчанию), RUB или EUR.
  • cost_mode: cpc (по умолчанию, цена клика) или cpm (значение делится на 1000).

Пример: источник с cost_param=cost, cost_currency=USD, cost_mode=cpc, ссылка /go/TOKEN?cost=0.0025&sub4=zone123. Каждый клик, включая повторный, получает свою цену. Пустые, отрицательные, бесконечные и нечисловые значения считаются нулём и не мешают переходу. Для POST /api/v1/clicks цена передаётся в query URL либо в поле тела "query":{"cost":["0.0025"]}; query URL имеет приоритет. Цена не наследуется от родительского клика.

Цена сохраняется с валютой и пересчитывается в RUB/USD/EUR по курсу трекера на дату клика. Если курса нет, сохраняются исходная цена и доступные пересчёты; недоступные пересчёты равны нулю.

В универсальном отчёте cost складывает цены кликов и расходы кабинетов остальных источников. Если у источника заполнен cost_param, расходы его кабинета и ручные расходы не учитываются: приоритет действует для всего запрошенного периода, даже если параметр отдельного клика отсутствует или содержит мусор. Это исключает двойной учёт. Изменение настройки влияет на выбор расходов кабинета в исторических отчётах; уже записанные цены кликов сохраняются.

Для разрезов, отсутствующих в расходах кабинетов (например sub1–sub10, оффер, регион), учитываются только цены кликов: распределять расходы кабинета по зонам без данных нельзя. Запрос dimensions: ["source_id","sub4"] поддерживает cost, profit, roi, profit_forecast и roi_forecast. Фильтры ботов и тестовых кликов применяются как к остальным метрикам кликов. Для потоков и сайтов дополнительно учитываются расходы кабинетов с привязкой кампаний.

Поддержка