Справочник по API

У Leadgram API одна задача: дать партнёрской сети, CRM рекламодателя или вашему собственному серверу работать с Leadgram по server-to-server, без человека в дашборде. Поверхностей три: приём конверсий на POST /api/events, чтение отчётов и ленты кликов на /api/v1/* и MCP-сервер на POST /api/mcp для разговорных клиентов вроде Claude или Cursor. Из коробки ключу открыт только приём конверсий - всё остальное выдаётся ключу отдельным правом и по умолчанию выключено.

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

Каждый запрос аутентифицируется заголовком x-api-key с секретом, который вы выпускаете в разделе Настройки → API-ключи. Как создавать, называть, ротировать и отзывать ключи - в API-ключах. Читающие ручки /api/v1/* и MCP-эндпоинт принимают только этот заголовок: куки дашборда там не креденшл вовсе.

Ключ - это не сессия. Из коробки он открывает один маршрут - POST /api/events - и больше ничего; таким остаётся каждый ключ, выпущенный до появления прав. Сверх этого права выдаются ключу по отдельности, тремя уровнями: read открывает чтение по ключу, write - создание и смену статуса, delete - то, что необратимо по последствиям. Уровень write при этом не значит «обратимое»: в нём лежит приём конверсии, который списывает деньги организации и шлёт событие в чужой рекламный кабинет. Право привязано к одной организации. Часть прав можно сузить до конкретных кампаний, но не любое: офферы и лендинги общие на всю организацию по устройству модели, из воронок сужается только funnels:delete, а из графов клика сужается всё, кроме click-flows:update. Несужаемое право выдаётся только ключу без списка кампаний, а грант, где стоят одновременно кампании и такой слаг, отвергается с кодом grant_scope_unsupported_resource - это правило, а не поломка. Сужение доезжает и до чтения: оба читающих слага тоже принимают список кампаний, и тогда totals в отчёте считаются по видимому ключу подмножеству, а не по всей организации. Полный список того, что можно выдать, лежит на странице API-ключей. Ни одним правом не выдаются боты и их токены, каналы, рекламные кабинеты, рассылки подписчикам, биллинг, участники команды, удаление организации, смена пароля, почты или аватара, отзыв сессий, свои домены клиента и расширение прав самого ключа; запрос ключом к такому маршруту отвечает 403 с кодом api_key_resource_forbidden, и это «здесь нужна сессия дашборда», а не «право не выдали»: выдать его нельзя ничем. Запрос к /api/auth/* с ключом отклоняется сразу с 403.

Ключ несёт только то, что ему выдали отдельно, и по умолчанию это ничего сверх приёма конверсий. Но выдать можно и чтение, и правку, и удаление, и тогда цена утечки растёт вместе с правом: держите по ключу на интеграцию и сужайте область до нужных кампаний там, где это разрешено. Обращайтесь с ним как с bearer-секретом: не вставляйте в публичный репозиторий, чат поддержки или общий скрипт, а всё, что утекло, - ротируйте.

POST /api/events

Ручка приёма конверсий: она записывает событие-конверсию на клик, принадлежащий вашей организации. Тело - JSON; минимум - это clickId плюс type. Единственной ручкой API она быть перестала - читающие /api/v1/* и MCP описаны ниже, - но единственной, что открыта ключу без отдельного права, осталась.

  • clickId (строка, обязательно) - id клика от Leadgram, то значение, что пришло к вам из макроса {click_id}. Клик чужой организации отклоняется.
  • type (строка, обязательно) - один тип события воронки или конверсии: land, bot_start, registration, lead, deposit, ftd, purchase и остальные из Событий и конверсий.
  • payout (число, необязательно) - сумма платящей конверсии (deposit, ftd, ...). Она питает колонку «Доход» в отчётах и уходит ценностью конверсии (USD) в Google Ads (conversionValue) и в TikTok (properties.value, на deposit, ftd, purchase). Meta её игнорирует: там ценность берётся из customData, а это поле по API не задать - оно вырезается из payload на входе. Строка вида "42.50" принимается, отрицательные значения отклоняются.
  • payload (объект, необязательно) - произвольная форма, прокидывается в событие. telegram_user_id внутри него - ключ для биллинга и дедупликации по пользователю; bot_username / bot_id называют бота для bot_start.

Отправка через curl:

curl -X POST https://leadgram.org/api/events \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-conversion-8813" \
  -d '{
    "clickId": "tz4k9m2p1xq7v0b3n8s6d5fg",
    "type": "deposit",
    "payout": 42.50,
    "payload": { "telegram_user_id": 123456789 }
  }'

Ответы:

  • 201 - записано. В теле - созданная строка события.
  • 400 - тело не прошло валидацию: нет clickId, неизвестный type, отрицательный payout. Тем же 400 с телом { "error": "Invalid JSON body" } отвечает нечитаемый JSON.
  • 401 - ключ прислан, но не принят: неизвестен, отозван, выключен или истёк.
  • 403 - причин несколько, и каждая, кроме двух первых, несёт свой машинный code. Клик не найден или принадлежит другой организации - тело { "error": "Click not found or access denied" } без code. Тем же 403 без code отвечает bot_start, у которого payload.bot_id или payload.bot_username называет бота чужой организации. Клик лежит в кампании вне области ключа - или вовсе без кампании, когда область выдана, - campaign_scope_forbidden. Слаг events не выдан этому ключу - api_key_write_not_granted; касается ключей, у которых права уже проставлены. Права ключа не удалось прочитать, либо в них не названа организация - api_key_grant_unverifiable. Владелец ключа больше не живой участник названной в правах организации - api_key_org_not_member. Его роль в ней больше не владелец и не администратор - role_forbidden, и роль перечитывается на каждом вызове. Отдельно от всего этого стоит запрос вообще без заголовка x-api-key: он отбивается раньше обработчика, с телом { "error": "CSRF: Origin header required for mutating requests" }, - это признак пустого или потерянного заголовка, а не проблемы с кликом. Кодов api_key_delete_not_granted и api_key_resource_forbidden на этой ручке не бывает вовсе: events лежит в уровне write и входит в плоский список того, что ключу разрешено в принципе.
  • 409 с кодом org_deleted - организация удалена. Ретраи бессмысленны, пока её не восстановят.
  • 413 с телом { "error": "Payload Too Large" } - тело JSON больше 1 МиБ. Размер проверяется дважды: по заголовку Content-Length и по реальной длине в байтах, поэтому занижение заголовка не помогает.
  • 422 - bot_start без telegram user id в payload (bot_start_missing_telegram_user_id); принять его - значит задвоить списание против старта, который Leadgram уже записал.
  • 429 - превышен лимит; отступите и повторите после окна (см. ниже).
  • 503 с телом { "error": "Service Unavailable" } и заголовком Retry-After: 60 - счётчик темпа недоступен, а для запросов по ключу политика отказа закрытая. Заголовков X-RateLimit-* тут нет вовсе: ведро никто не считал, и утверждать его состояние ответ не вправе. Это не «вы выбрали квоту», как 429: повторите через минуту, длинный backoff не нужен.

Чтение по ключу (v1)

Читающие ручки живут под https://leadgram.org/api/v1 и открываются правом уровня read: слаг clicks - лента кликов, слаг report - сводка. Обе только GET; POST отвечает 405 с кодом method_not_allowed.

Окно наблюдения задаётся парой date_from и date_to в ISO 8601. Границы включительные с обеих сторон, время за вас не дописывается: date_to=2026-09-05 означает полночь. Максимальная ширина окна - 92 дня, шире - 400 window_too_wide, и диапазон надо разбить на несколько запросов.

Что обещает v1: поля не удаляются и не меняют тип, новые поля могут появиться в любой момент - клиент обязан игнорировать незнакомые, - а в перечислимых полях могут появиться новые значения. Удаление поля, смена типа, смена смысла поля, смена модели пагинации или окна по умолчанию потребуют v2, и он будет жить рядом, а не вместо: v1 продолжит работать, а о выводе из эксплуатации предупредят заголовки Deprecation и Sunset с датой не ближе чем через 180 дней. POST /api/events версии в пути не имеет и не получит: этот URL напечатан в гайдах и лежит в конфигах уже развёрнутых интеграций.

GET /api/v1/clicks

Постраничная лента кликов организации, от новых к старым. Все параметры необязательны.

  • date_from, date_to - ISO 8601. Обе пусты - отдаются последние 30 дней до текущего момента; задана одна - вторая достраивается тем же интервалом в 30 дней.
  • campaign_id - фильтр по кампании. Не валидируется: кампания вне области ключа даёт пустой срез, а не отказ.
  • status - ровно одно из clicked, landed, started, registered, lead, deposit, ftd. Опечатка даёт 400 invalid_status, а не тихо пустую ленту.
  • tracked - true (по умолчанию), false или all.
  • cursor - строка next_cursor предыдущей страницы, передаётся дословно.
  • limit - от 1 до 1000, по умолчанию 100. Значение вне диапазона клампится к границе, а не отвергается.
curl -s -H "x-api-key: YOUR_API_KEY" \
  "https://leadgram.org/api/v1/clicks?date_from=2026-09-01T00:00:00Z&date_to=2026-09-07T00:00:00Z&limit=500"

Пагинация курсорная (keyset по паре «время создания, id»), а не по номеру страницы: на живой ленте смещение пропускает и дублирует строки между страницами. В ответе { "data": [...], "next_cursor": ... }, где next_cursor - строка, если следующая страница есть, и null, если её нет. Поля total здесь нет и не будет: keyset не даёт дешёвого счётчика, а врущий счётчик хуже отсутствующего - за точными числами идите в /api/v1/report.

Каждый элемент data - 21 поле белого списка, а не строка таблицы: id, created_at, campaign_id, status, tracked, geo, device, os, browser, ref_id, offer_id, sub1-sub9 и revenue. created_at - ISO 8601 в UTC. revenue - десятичная строка как есть, без конвертации в число, либо null, если подтверждённых выплат по клику не было. Не отдаются поимённо: ip_hash, user_agent, visitor_id, visitor_is_new, fbclid, gclid, gbraid, wbraid, ttclid, click_flow_node_path, ad_account, bot_id, platform, org_id и suppressedDelivery - это идентификаторы посетителя, склейка того же человека с чужими рекламными системами и внутреннее устройство маршрутизации.

GET /api/v1/report

Сводная воронка организации за окно в одном срезе. Единственное место, где интегратор получает точные счётчики.

  • dimension - обязателен, ровно одно из campaign, geo, sub1, offer, day. Отсутствует или незнаком - 400 invalid_dimension.
  • date_from и date_to - оба обязательны, в отличие от ленты кликов: сюда ходит скрипт, и запрос без окна означал бы агрегат по всей истории на каждый вызов. Не хватает границы - 400 window_required.
  • campaign_id - необязателен; кампания вне области ключа даёт пустой срез, а не 403: отчёт не подтверждает существование чужой кампании.

Пагинации у отчёта нет и не будет. Срез режется потолком в 500 строк, и упор в потолок приезжает в теле успешного 200: truncated: true, code: "result_truncated" и подсказка сузить окно или добавить campaign_id. Итоги totals при этом считаются по всему срезу, до потолка, поэтому верны и там, где строк больше 500. «По всему срезу» - это по всему, что видит ключ: область кампаний из его прав уходит в тот же WHERE, что и строки, поэтому у ключа с областью totals - итоги его кампаний, а не итоги организации.

В ответе - dimension, window с границами в UTC, rows, totals и truncated. Каждая строка rows несёт key, label, clicks, landed, started, registered, lead, deposit, ftd и revenue. Поля cost и cost_source есть только у срезов campaign, sub1 и day: ключ таблицы расходов раскладывается ровно по ним, а у geo и offer этих полей нет вовсе - не null, а отсутствуют. Деньги - десятичная строка с двумя знаками, revenue без данных равен "0.00".

Коды отказа читающих ручек

Форма отказа одна на обе ручки - error, машинный code и hint, - и code это часть контракта: на него можно писать switch.

  • 400 - invalid_window, window_too_wide, window_required, invalid_dimension, invalid_status, invalid_tracked, invalid_cursor. Все чинятся запросом.
  • 401 api_key_required - нет заголовка x-api-key.
  • 401 unauthorized - ключ неизвестен, отозван, выключен или истёк.
  • 403 api_key_scope_unverifiable - права ключа не удалось прочитать. Это «у нас проблема», а не «вам не выдали»: повторите через момент, а если повторяется - перевыпустите права ключа.
  • 403 api_key_grant_unverifiable - права есть, но в них не названа организация. Грант надо выдать заново, привязав к одной.
  • 403 api_key_org_not_member - владелец ключа больше не участник названной организации. Проверяется на каждом вызове.
  • 403 api_key_read_not_granted - слаг clicks или report этому ключу не выдан. Именно 403, а не 404: существование публичного API задокументировано, и 404 отправил бы вас искать опечатку в URL.
  • 403 role_forbidden - роль владельца ключа в его организации больше не владелец и не администратор. Роль перечитывается на каждом вызове, так что ключ не переживает исключения владельца из команды.
  • 409 org_deleted - организация удалена.
  • 429 rate_limited - выбран потолок темпа, в заголовках Retry-After и X-RateLimit-*; сами потолки - в разделе Лимиты и идемпотентность.
  • 503 на этой поверхности не бывает вовсе: при недоступном счётчике темпа чтение пропускается, а не запирается.

MCP-сервер

Разговорные клиенты - Claude, Cursor и другие хосты MCP - ходят в Leadgram через POST https://leadgram.org/api/mcp. Транспорт - Streamable HTTP без сессий, ответ приходит обычным JSON, а не потоком SSE; GET на этот же адрес отвечает 405 с кодом JSON-RPC -32000, и это верный ответ протокола, а не недоделка: потока server→client в режиме без сессий не бывает. Разрешающих заголовков CORS на маршруте нет ни одного - страница из браузера сюда не ходит.

Аутентификация - тот же x-api-key и только он, куки дашборда MCP не принимает. Отказы приходят в форме JSON-RPC, а не привычным телом с error, code и hint: -32001 с HTTP 401, когда ключа нет или он неизвестен, отозван, выключен либо истёк, и -32002 с HTTP 503, когда подлинность ключа не удалось проверить прямо сейчас - отдельный ответ вместо 401 затем, чтобы оператор не пошёл перевыпускать рабочий ключ. Каталог инструментов до проверки ключа не отдаётся вовсе.

В каталоге 25 инструментов, и он не фильтруется по правам: tools/list отдаёт один и тот же список любому владельцу живого ключа, независимо от того, что этому ключу выдано. Отказ приходит уже на вызове инструмента и несёт готовую инструкцию - попросить владельца или администратора организации выдать нужный слаг. Каждый инструмент объявляет пару «слаг гранта - настоящий маршрут» и исполняется на этом маршруте, а не внутри /api/mcp, поэтому на него работают ровно те же рельсы: аллоулист, право со своим уровнем, роль владельца ключа, счётчик темпа и идемпотентность. Сверх них стоит гейт исполнения: слаг обязан лежать в массиве своего уровня выданного гранта, иначе 403 api_key_write_not_granted или 403 api_key_delete_not_granted - код называет уровень, во что вы упёрлись.

  • Чтение: leadgram_list_clicks (clicks), leadgram_get_report (report).
  • Уровень write: leadgram_report_conversion (events), leadgram_create_campaign (campaigns), leadgram_create_offer (offers), leadgram_set_offer_status (offers:status), leadgram_create_landing (landings), leadgram_set_landing_status (landings:status), leadgram_create_funnel (funnels), leadgram_create_click_flow (click-flows), leadgram_set_click_flow_status (click-flows:status).
  • Уровень delete: leadgram_update_campaign (campaigns:update), leadgram_delete_campaign (campaigns:delete), leadgram_update_offer (offers:update), leadgram_delete_offer (offers:delete), leadgram_update_landing (landings:update), leadgram_delete_landing (landings:delete), leadgram_update_funnel (funnels:update), leadgram_delete_funnel (funnels:delete), leadgram_delete_click_flow (click-flows:delete), leadgram_publish_click_flow (click-flows:publish), leadgram_create_postback (postbacks), leadgram_update_postback (postbacks:update), leadgram_delete_postback (postbacks:delete), leadgram_record_ad_spend (ad-spend).

Инструмента для слага click-flows:update в каталоге нет намеренно: парного чтения графа под ключ не существует, поэтому разговорному клиенту была бы доступна ровно одна ветка - затереть существующий граф вслепую. По HTTP этот слаг выдаётся как обычно.

Подсказка destructiveHint: true - то есть «хост обязан спросить подтверждение» - стоит у шестнадцати инструментов из двадцати пяти: у каждого инструмента уровня delete, а сверх них у leadgram_report_conversion (приём конверсии списывает деньги организации и уходит в чужой рекламный кабинет) и у leadgram_set_click_flow_status (уход графа из активного статуса немедленно снимает привязанные активные кампании на запасной редирект).

Два предупреждения из описаний самих инструментов. leadgram_update_offer и leadgram_update_landing заменяют строку целиком: не переданные поля очищаются, а прочитать текущие значения этим же ключом нельзя. leadgram_create_postback заводит правило только с методом доставки GET, и сменить метод после создания нельзя нигде - правило на POST заводят из кабинета.

Ключ, выпущенный до появления прав, шлёт конверсии на POST /api/events без гранта - на MCP это исключение не действует. Срок совместимости выдан не ключу вообще, а конкретной настроенной поверхности по HTTP; в MCP такой ключ никогда не был настроен, вставать нечему. Поэтому слаг events здесь обязан быть выдан грантом, иначе придёт 403 api_key_write_not_granted с отдельной подсказкой про то, что исключение покрывает только HTTP-ручку. То же и с ролью: послабление для ключей, выпущенных до 2 сентября 2026 года, тоже остаётся на HTTP.

Постбэки (исходящие)

Постбэки идут в обратную сторону: на каждую конверсию, которую Leadgram записывает, он server-to-server дёргает S2S-URL вашего партнёрского трекера, чтобы регистрации и депозиты оставались видны в Keitaro или RedTrack, где вы оптимизируете. Настраиваются они в приложении или ключом, которому отдельно выдан уровень delete на слагах postbacks, postbacks:update и postbacks:delete. Уровень именно разрушительный, и это не перестраховка: адрес правила - произвольный внешний URL, правило стреляет на каждой будущей конверсии бессрочно, а правило без кампании действует на все кампании организации, включая будущие, - то есть ключ с таким правом вправе переписать адрес постбэка на чужой. Само правило - это триггер-событие, шаблон URL и метод, сохранённые на Постбэках. Пресеты предзаполняют шаблон: Keitaro, RedTrack или Custom с чистого листа, с макросами вроде {click_id}, {event_type} и {sub1}-{sub5}, которые подставляются при каждом срабатывании. Payout макросом постбэка не является - он едет с событием, а не с постбэком. Полный список макросов, порядок связки и логика ретраев - в гайде по постбэкам.

Лимиты и идемпотентность

Эндпоинт событий держит примерно 1800 запросов в минуту на организацию. Сверх этого приходит 429 с заголовком Retry-After и заголовками X-RateLimit-*, называющими потолок и момент сброса, - выждите указанный интервал вместо того, чтобы долбить.

У читающих ручек потолки свои: GET /api/v1/report - 60 запросов в минуту, GET /api/v1/clicks - 300 запросов в минуту. Отчёт на порядок дороже страницы ленты, отсюда и разница. Все три потолка - и приём конверсий, и обе читающие ручки - считаются на организацию, а не на ключ: у ключа нет собственной идентичности в счётчике, поэтому два ключа одной организации делят квоту, и выпуск второго ключа не удваивает разрешённую нагрузку. Разница между чтением и записью в аварии тоже названа: при недоступном счётчике чтение пропускается и 503 на /api/v1/* не бывает, а запись по ключу, наоборот, отвечает 503 с Retry-After: 60.

Ретраи безопасны, если сделать их идемпотентными. Пришлите заголовок Idempotency-Key (или поле nonce в теле), и id события выведется из clickId + type + этого значения, поэтому повтор того же запроса не создаст вторую строку и не задвоит ни списание, ни постбэки, ни конверсии в рекламных кабинетах. Обратная сторона: две действительно разные конверсии по одному клику - второй депозит - обязаны нести разный ключ, иначе вторая схлопнется в первую и пропадёт. Полная модель дедупликации - в Событиях и конверсиях.