API reference
The Leadgram API exists for one job: letting an affiliate network, an advertiser's CRM, or your own server report conversions back to Leadgram server-to-server, without a human touching the dashboard. The surface is deliberately tiny - a single write endpoint, one credential, and no read access. Everything else lives in the browser.
Authentication
Authenticate every request with the x-api-key header carrying a secret you mint under Settings → API keys. See API keys for how to create, name, rotate and revoke them.
A key is not a session. It unlocks exactly one route - POST /api/events - and nothing else. Clicks, postbacks, reports, campaigns, subscribers, billing, even GET /api/events: every other endpoint and every dashboard page treats a key as no credential at all and answers 401 or 403, whatever its owner can do in the browser. A request to /api/auth/* carrying a key is refused outright with 403.
An API key is events-only. It cannot read your data, change campaigns or flows, touch billing, or perform any destructive action - a leaked key can write conversions into your organization, and nothing more. Treat it as a bearer secret: never paste it into a public repo, a support chat or a shared script, and rotate anything that leaks.
POST /api/events
One endpoint, one method. It records a conversion event against a click your organization owns. The body is JSON; the minimum is clickId plus type.
clickId(string, required) - the Leadgram click id, the value that reached you through the{click_id}macro. A click belonging to another organization is rejected.type(string, required) - one funnel or conversion event type:land,bot_start,registration,lead,deposit,ftd,purchase, and the rest listed in Events and conversions.payout(number, optional) - the amount on a paying conversion (deposit,ftd, ...). It feeds the Revenue column in Reports and becomes the value sent to the ad platform. A numeric string like"42.50"is accepted; negatives are rejected.payload(object, optional) - free-form, passed through onto the event.telegram_user_idinside it keys billing and per-user dedupe;bot_username/bot_idname the bot for abot_start.
Send it with 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 }
}'
Responses:
201- recorded. The body is the created event row.400- the body failed validation: a missingclickId, an unknowntype, a negativepayout.401- the key was not accepted, or no credential was sent at all.403- the click was not found or belongs to another organization (also what a key gets on any route other than this one).422- abot_startwith no Telegram user id inpayload(bot_start_missing_telegram_user_id); accepting it would double-bill against the start Leadgram already recorded.429- you exceeded the rate limit; back off and retry after the window (see below).
Postbacks (outbound)
Postbacks run the other direction: for every conversion Leadgram records, it calls your partner tracker's S2S URL server-to-server, so registrations and deposits stay visible in Keitaro or RedTrack where you optimize. They are configured in the app, not driven by the API key - a rule is a trigger event, a URL template and a method, saved on Postbacks. Presets prefill the template: Keitaro, RedTrack, or Custom from blank, with macros like {click_id}, {event_type} and {sub1}-{sub5} substituted on every fire. Payout is not a postback macro - it travels with the event, not the postback. The full macro list, the wiring order and retry behavior are in the Postbacks guide.
Limits & idempotency
The events endpoint allows about 300 requests per minute per organization. Past that you get 429 with a Retry-After header plus X-RateLimit-* headers naming the ceiling and when it resets - wait out the stated interval rather than hammering.
Retries are safe once you make them idempotent. Send an Idempotency-Key header (or a nonce field in the body) and the event id is derived from clickId + type + that value, so resending the same request never writes a second row or double-fires the charge, the postbacks or the ad-platform conversions. The flip side: two genuinely different conversions on the same click - a second deposit - must carry a different key, or the second one collapses into the first and disappears. See Events and conversions for the dedupe model in full.