How tracking works
AlterCPA TOP is a postback source. When a lead changes state, we fire an HTTP request to the URL you configured for that event — straight into your tracker. We only send postbacks; we never receive them.
The {click} you embed in your deep link is the bridge: it travels from your
ad → your tracker → AlterCPA TOP → back to your tracker when we fire the
postback. Your tracker uses it to match our postback to its original click record.
The five events (= lead statuses)
| Event | Fires when | Conditional? |
|---|---|---|
wait | Subscriber pressed Start in the bot | No — always fires |
hold | Join request submitted (approval-required chats) | No — always fires |
approve | Subscriber joined, or request was approved | No — always fires |
cancel | Subscriber left voluntarily | Only within the unsub window |
trash | Subscriber was kicked or banned | Only within the unsub window |
A leave after the unsub window still moves the lead to cancel / trash
but fires no postback. The window is configured per campaign.
Macro reference
Macros are substituted into your postback URLs before we fire them — and they
work just as well inside the message itself: the welcome text, screen text
and broadcast body, plus every button URL (the CPA button, screen link buttons,
the broadcast CTA). So you can drop a tracking link straight into the text — e.g.
a Markdown link pointing at https://track.example/?click={click} — and {click}
is filled in for each subscriber, exactly as it is in a postback. The same macro
set applies everywhere.
| Macro | Value |
|---|---|
{click} | The click id from the deep link — the join to your tracker |
{status} | wait / hold / approve / cancel / trash |
{campaign} | Campaign code |
{user} | Your AlterCPA TOP user id |
{lead} | Internal lead id — useful as a dedup key |
{subscriber} | Subscriber’s Telegram user id |
{username} | Subscriber @username (may be empty if the user has none) |
{chat} | Tracked chat id |
{time} | UNIX timestamp of the event |
Unknown macros are left untouched. Empty values become empty strings.
Global vs per-campaign templates
You have two places to set postback URLs:
- Profile (global) — five templates set once and reused everywhere.
- Campaign (per-campaign) — five templates for a specific campaign.
The rule is all-or-nothing per campaign:
- Campaign sets any of its five templates → uses its own set as-is (empty fields in the campaign mean “no postback for that event”).
- Campaign sets none → falls back to the global templates from your Profile.
This means if you override one postback on a campaign you have to set all five you care about on that campaign — there’s no per-event fall-through.
Delivery rules
- GET only. We issue a plain HTTP GET. Every tracker we target (AlterCPA Lite, Keitaro, Binom, …) accepts GET postbacks.
- Public hosts only. Your URLs are checked against an SSRF blocklist — loopback, private/RFC1918 ranges, link-local, and cloud-metadata IPs are all refused. Only public hostnames and IPs are accepted.
- Retried with backoff. A failure is re-queued with a growing delay — 1 min, then 5 min (three delivery attempts in total). After the last one the event is logged as failed.
- Success = HTTP 2xx (and a
{"status":"ok"}body for AlterCPA Lite). - Idempotent per lead/event — we don’t re-fire an event for the same lead unless a new transition actually occurs.
The unsub window
The window is measured from the moment of joining. A leave always moves the lead
to cancel or trash, but whether a postback fires depends on the window:
| Leave relative to the window | Lead status | Postback |
|---|---|---|
| Inside the window | cancel / trash | fires |
| Outside the window | cancel / trash | suppressed |
Window of 0 means “always fire” (no expiry). Typical values: 1 day, 7 days,
30 days. Set per campaign in the editor.
Next steps
Choose your tracker to get a ready-to-paste postback setup:
- AlterCPA Lite — recommended; one-click auto-setup
- AlterCPA Pro
- Keitaro
- Binom
- Any other tracker