Events and conversions: the money layer of your funnel

Clicks are a cost until an event proves they paid off. In Leadgram every conversion - a bot start, a registration, a deposit - is an event tied to one specific click, and that link drives everything downstream: revenue in Reports, postbacks to partner trackers, the server-side conversions you feed back to Meta and Google, and a push notification back to the subscriber who converted, if your flow wires one up. Get events flowing correctly and the rest of the tracker starts telling you the truth about your spend.

Event types

  • Funnel events - click, land, bot_start, channel_join, join_request, first_dm, miniapp_launch. They describe how a user moves through your Telegram funnel.
  • Conversion events - lead, registration, deposit, ftd, purchase. These are the events advertisers pay you for.
  • Custom events - custom_1 through custom_8 for anything offer-specific.
  • Telegram Stars subscriptions - subscription_renewed and subscription_cancelled. They come from the subscription update Telegram sends your bot: state active (the user re-enabled the subscription) becomes subscription_renewed, states canceled and failed become subscription_cancelled. The recurring charge itself arrives separately, as a purchase.

The click-status ladder on the Clicks page is Clicked → Landed → Started → Registered → Lead → Deposit → FTD, and exactly six types move it: land → Landed, bot_start → Started, registration → Registered, lead → Lead, deposit → Deposit, ftd → FTD. Status only moves forward - a late registration arriving after an ftd never downgrades the click. Every other type leaves the status alone: click, channel_join, join_request, first_dm, miniapp_launch, purchase, custom_1-custom_8, subscription_renewed and subscription_cancelled are all recorded and reported, they just never touch the ladder. purchase is where that surprises people most: it's billed like any other conversion, and the click's status stays where it was.

How events arrive

  1. S2S / API. Your backend (or the advertiser's postback script) calls POST /api/events with the clickId, the event type, and optionally payout and nonce. Create an API key under Settings → API keys for external tools like Keitaro or your own scripts. Once permissions have been saved on that key even once, events has to be among them - without it the conversion is refused with 403 api_key_write_not_granted. A key that has never had permissions saved keeps posting conversions over HTTP exactly as before. Reading the data back is a different surface: GET /api/events is the dashboard screen and resolves a session only, so a key sent to it gets 401. What a key reads is GET /api/v1/clicks (the click feed, one row per click) and GET /api/v1/report (the funnel summary with counts), each once the matching clicks or report read permission is granted to that key - parameters, response shapes and status codes are in the API reference.
  2. Flows. Drop an Event node into a Message Flow and the event fires automatically when a subscriber reaches that step - registrations and deposits straight out of your Telegram funnel, no code.
  3. Leadgram itself. A few types are emitted by the tracker from the Telegram updates your bot already receives. The next section says exactly which ones - and which ones will stay empty until you send them.

Deduplication is deterministic: the same clickId + type without a nonce is treated as a retry of the same conversion and never creates a second row, a second charge or a duplicate Meta purchase. A genuine repeat conversion (a second deposit from the same click) must send a unique nonce.

Which types Leadgram produces itself

Being listed in the Event node, mapped to a Meta event and accepted by the API does not mean a type is tracked for you. Three groups, and the difference decides whether you should be waiting on a number or sending it:

  • Produced by Leadgram - bot_start, channel_join, join_request, purchase, subscription_renewed and subscription_cancelled come out of the Telegram updates your bot already receives; miniapp_launch is emitted server-side, but only once the snippet from Mini Apps calls the launch endpoint - Telegram announces a Mini App launch to nobody. Connect the bot or the channel and the update-driven types start flowing on their own, with one condition: an event is written only when the update can be tied to a click. What can't be tied becomes no event at all and is counted under Unattributed drops on Delivery Health. That bites hardest on bot_start - someone who opens the bot by username instead of your tracking link never produces one.
  • Accepted from outside only: click, land - the redirect writes a click row on the Clicks page, not an event, and nothing observes your landing page. A click's status therefore never reaches Landed unless you send a land event yourself.
  • Accepted from outside only: first_dm - Bot API webhooks only cover chats with your own bot; catching a first DM to a personal account needs an MTProto user session, which Leadgram does not run. Fire it from an Event node or post it to POST /api/events.
  • Yours to send - lead, registration, deposit, ftd and custom_1-custom_8. Only your backend knows when the advertiser confirmed one.

Sending a type from the first group yourself is allowed and sometimes correct (your own billing reporting purchase, for instance), but it writes a second event next to the automatic one. bot_start is the exception: the webhook, an Event node and the API collapse onto one event per user, so reporting a start Leadgram already saw does not double it. That collapse is keyed on the pair (bot, Telegram user), which is why the API requires the user for this type - send it as telegram_user_id, tg_user_id, from.id or user.id inside payload. A bot_start without it is refused with 422 (bot_start_missing_telegram_user_id) rather than written as a second start that would reach Meta as a second Lead.

What a doubled start actually costs. Not money: you pay once per person per workspace - the charge fires on their first billable event, and a Telegram account is matched with no expiry - so a second bot_start from someone already paid for settles as free and your balance never sees it. The damage is downstream. The duplicate carries a different event_id, so Meta's own deduplication cannot merge it with the first one, and your campaign optimizer is taught that one person converted twice - you then pay for that lesson in every auction it bids in afterwards. The funnel counts in Reports drift by the same event. That is the reason for the pairing rules below, not a second invoice.

Two conditions come with that pairing, and both are worth stating plainly:

  • Over the API, something has to name the bot. Send bot_username (the @name) or bot_id inside payload, or report a clickId whose click carries a bot - clicks inherit the bot of their campaign, while a click from a plain redirect campaign has none, and neither does one whose bot was deleted. A report that names no bot either way cannot name the pair, so it is recorded as its own separate event instead of joining the shared one. Naming the bot explicitly also protects the multi-bot case: the click's bot is only a fallback guess, and it is the wrong one when the human started a different bot of yours.
  • An Event node joins the pairing only for flow runs that recorded the Telegram user. Runs started before this shipped did not, and their user cannot be reconstructed after the fact. For those the node emits nothing for bot_start instead of guessing - the webhook already recorded that start anyway. The node stays silent for another reason too: a flow run with no click behind it has nothing to attribute a start to, so instead of an event that could never reach Meta, the loss is counted under Unattributed drops on Delivery Health.

"Accepted from outside only" describes the code as it is, not a feature on the way. If nothing sends first_dm, no first_dm row will ever exist - being selectable in an Event node and mapped to Meta Contact does not make it automatic.

Retargeting: a second click for the same person

A retargeting re-click is the ordinary case: the same human clicks your ad again, gets a fresh click row, and starts the bot again. bot_start is counted once per bot and Telegram user, permanently, so the second start writes no new event, no new charge and no second Meta Lead. That is deliberate - the alternative is two Leads for one person, which corrupts the optimizer you are paying to train.

The cost of that choice is visible in one place: the new click keeps the status Clicked on the Clicks page even though the person did open the bot. Nothing advances it, because nothing new happened at the event level. When you measure a retargeting campaign, count starts from the events, not from click statuses.

The billing gate

Recording an event and delivering it downstream are two different things. Every event row carries a billing_status: it starts pending while the debit runs, then settles to charged or free. CAPI dispatch, postbacks and event-triggered pushes to the converting subscriber only fire once that status resolves to charged or free - a paused org or an empty balance leaves the row blocked (a failed debit leaves it error), and the event sits in the database without ever reaching Meta, Google, your postback endpoint or the subscriber.

If a conversion shows up in the events list but nothing fired on the other end, check billing before you check the integration. A blocked event isn't broken - it's doing exactly what it's supposed to.

The payout field

Pass payout (USD, up to two decimals) with every paying conversion. It feeds two places:

  • Reports revenue - the revenue column is the sum of payouts across confirmed events.
  • Google Ads - payout becomes conversionValue on the server-side conversion, so Smart Bidding optimizes toward real money.

A conversion that arrives without a payout of its own inherits the Payout of the offer attached to the click: the platform looks the offer up by the offer_id stamped on the click. An explicit payout on the event always wins. A click that never passed through an offer has nothing to inherit, so the amount stays empty.

Postbacks don't carry payout. The macros available to partner trackers are {click_id}, {event_type}, the sub-chain and the platform-native click ids - see Postbacks for the full list. If your tracker needs the conversion amount, pass it back through your own integration.

What the CAPI column means

The Dashboard's Recent events table shows a CAPI status per event, and there are three of them. No attribution - the event has no click behind it; that check runs first, and neither CAPI nor a postback will ever fire for such a row. Sent - the current attempt has at least one successful delivery and no failed ones; skips don't count, so on a campaign that pairs Meta with Google a click carrying no gclid, gbraid or wbraid is skipped by Google as designed and the badge still reads Sent. Pending - everything else, including events with no deliveries at all: the campaign has no integrations, every platform skipped, or the billing gate never resolved to charged or free. Then the badge sits on Pending with an empty queue. Per-platform detail - attempts, last error, one-click re-fire - lives on Delivery Health.

The safe test event

On the Facebook page each Meta CAPI account has a Send test event button. You pick the event name: the dropdown offers eight - Lead, SubmitApplication, Subscribe, CompleteRegistration, Contact, ViewContent, PageView, Purchase - and the default is Lead, what bot_start maps to. The event goes out synthetic, with your Test Events code from Meta Events Manager, so it shows up in the Test Events tab and never touches ad optimization, attribution or billing. It's the fastest way to prove your pixel and token work before spending a dollar. The button refuses to fire without a code - an uncoded event would count as real. Pick the name your funnel actually produces: you set the campaign objective from what you saw in Test Events, and testing Purchase on a funnel that sends Lead leaves Ads Manager blank.

Common pitfalls

  • Skipping nonce on repeat conversions - the second deposit is silently treated as a retry and dropped.
  • Sending events without payout on clicks with no offer to inherit one from - Reports revenue stays at zero and Google gets valueless conversions.
  • Firing registration for a click that already reached ftd and expecting the status to change - it won't, by design.
  • Expecting purchase to move the click's funnel status - it never does, even though it's billed like any other conversion.
  • Reading Pending in the CAPI column as a bug - check Delivery Health first; it usually names the exact platform error.
  • Assuming a recorded event was delivered - blocked, error and pending billing states mean it never reached CAPI, postbacks or pushes.
  • Reconciling with GET /api/events and an API key - that endpoint answers a dashboard session only and returns 401 to a key; reconcile through GET /api/v1/clicks and GET /api/v1/report.
  • Judging a retargeting campaign by click statuses - a re-click by someone who already started the bot stays at Clicked by design, because the start is counted once per person.
  • Waiting for miniapp_launch to appear on its own - Telegram sends no launch update; the number stays at zero until the Mini Apps snippet is live.