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_id inside it keys billing and per-user dedupe; bot_username / bot_id name the bot for a bot_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 missing clickId, an unknown type, a negative payout.
  • 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 - a bot_start with no Telegram user id in payload (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.