Skip to content

Postback

slimTDS handles two directions of postback traffic: incoming (partner networks notifying slimTDS of conversions) and outgoing (slimTDS forwarding conversion signals to your own tracking endpoints). Both are decoupled from the click pipeline so neither blocks visitor redirects.

Conversions log in the slimTDS admin

Partner networks call /postback to report conversions. Both GET (query string) and POST (form-encoded or JSON body) are accepted. Query-string parameters take precedence over body keys when both are present.

ParameterNotes
tokenPostback token — required. Either a per-offer token or a campaign-level catch-all token.
subidClick ID ({click_id} macro value placed in the offer URL). Required for offer-tokens; optional for campaign-tokens.
payoutNumeric payout amount (defaults to 0).
statusOne of approved, pending, hold, rejected (defaults to approved).
external_idPartner’s own conversion identifier (stored for cross-reference).

slimTDS resolves the token in two steps:

  1. Per-offer token — each offer has a unique postback_token (a random 32-hex-char string). When this token is used, the conversion is attributed to that specific offer. The subid must be a valid click ID that was already recorded in stats.clicks.
  2. Campaign-level catch-all token — each campaign also has a postback_token (a 32-hex UUID-derived string). This allows a single postback URL to handle any offer inside the campaign; the actual offer is resolved from the click row using subid. If no subid is supplied the ping is recorded as an anonymous campaign conversion (see below).

If the token matches neither an offer nor a campaign, the endpoint returns 404.

Conversions are upserted into core.conversions with a unique index on click_id. Sending the same subid twice updates payout, status, external_id, and updated_at rather than creating a duplicate row. PostgreSQL NULLs are treated as distinct, so multiple anonymous pings (no subid) can coexist without conflict.

When a campaign-level token is used without a subid — for example, a partner’s test button that does not propagate click IDs — slimTDS records a conversion row with click_id = NULL and offer_id = NULL, tied only to the campaign. This allows operators to count partner events even when per-click attribution is unavailable.

When a conversion arrives and the matched offer has one or more postback URLs configured (postback_urls JSONB array on core.offers), slimTDS enqueues one core.postback_deliveries row per URL. The row is created synchronously during the incoming postback request, but the HTTP call is never made from the request thread.

The postback:deliver cron command (schedule: * * * * *, every minute) picks up to 50 pending rows and fires each one as an HTTP GET via cURL. On success (HTTP 2xx or 3xx) the row is marked delivered_at = now(). On failure (4xx, 5xx, or network error) the attempt count is incremented and the next retry is scheduled at an exponentially increasing delay.

Delays are indexed by attempt number (0-based). Up to 5 total attempts are made.

AttemptDelay before retry
1st retry1 minute
2nd retry5 minutes
3rd retry25 minutes
4th retry2 hours
5th retry10 hours

After 5 failures the row is no longer picked up. Total retry budget is approximately 13.5 hours from the first failure.

Postback URL templates are expanded before delivery. The following tokens are supported:

TokenResolves toAvailable in outgoing postbacks
{click_id}The click ID (UUIDv7) originally passed as {click_id} in the offer URLYes
{payout}Payout amount from the incoming postbackYes
{status}Conversion status (approved, pending, hold, rejected)Yes
{external_id}Partner’s external_id valueYes
{currency}Currency code from the offer (e.g. USD)Yes
{visitor_uuid}Persistent visitor UUIDNo — left literal
{country}ISO 3166-1 alpha-2 country codeNo — left literal
{region}Region / subdivisionNo — left literal
{city}City nameNo — left literal
{device}Device type (mobile, tablet, desktop)No — left literal
{os}Operating systemNo — left literal
{browser}Browser nameNo — left literal
{lang}Accept-Language valueNo — left literal
{ip}Visitor IP addressNo — placeholder 0.0.0.0
{ua}User-Agent stringNo — placeholder -
{referer}HTTP RefererNo — left literal
{lander_host}Originating lander hostnameNo — left literal
{lander_domain}Lander domain without TLDNo — left literal
{lander_button}Lander button/path segmentNo — left literal
{campaign_slug}Campaign slugNo — left literal
{timestamp}Unix timestampNo — delivery time, not click time
{utm_source}UTM sourceNo — left literal
{utm_medium}UTM mediumNo — left literal
{utm_campaign}UTM campaignNo — left literal
{utm_term}UTM termNo — left literal
{utm_content}UTM contentNo — left literal
{rand:MIN-MAX}Random integer between MIN and MAXNo — left literal
{randstr:N}Random Base58 string of length NNo — left literal

Unrecognised tokens are left as-is in the URL.