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