Довідник з 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 + цього значення, тож повтор того самого запиту не створить другий рядок і не задвоїть ні списання, ні постбеки, ні конверсії в рекламних кабінетах. Зворотний бік: дві справді різні конверсії по одному кліку - другий депозит - мають нести різний ключ, інакше друга схлопнеться в першу і зникне. Повна модель дедуплікації - в Подіях і конверсіях.