Every time a new client hooks into our postback API, the same conversation repeats in chat: “What is click_id?”, “Where do I get macros?”, “How is s2s different from a regular callback?”. There are about ten terms in this space, but they migrate between trackers under different names, and keeping them all in one head is hard.

This piece is a short reference. No long-form discussion: each term gets a definition, a reason, an example. At the end — a working curl scenario and a debug checklist for when the postback does not arrive.

TL;DR

Click — the moment of the link follow. Conversion — the target action (order, lead). Postback = S2S callback = a server-to-server HTTP request from your server to ours, stitching click and conversion together. The key is click_id. Macros are tokens like {click_id} in the postback URL template, substituted with real values when fired. Hash is the signature that proves the postback came from you and not a random script.

Click

The moment a user followed your short link (tds.so/abc or go.brand.com/abc). At that moment we:

  • generate a unique click_id (~10 characters);
  • store everything we know in the database: country, device, source (UTM), timestamp, IP, fingerprint;
  • apply smart-link routing rules to pick the destination URL;
  • 302-redirect to the destination, appending ?cid=<click_id> to the query string.

Click is what we know up-front. After that the user goes to your landing, and we no longer see anything until you call us back through a postback.

Conversion

The target action on your site or CRM: order paid, lead form submitted, signup confirmed. The thing the campaign was launched for. Conversions come in three shapes:

  • With money — the order value is known in minor units (cents/kopeks), passed as value.
  • Without money — lead, signup, subscription. Pass average or zero value.
  • Multi-step — signup → email-confirm → first purchase. Each step is a separate postback with a different event.

Postback (a.k.a. S2S callback)

An HTTP request (usually POST or GET) from your server to our API at conversion time. Contains click_id, event type, and amount. We receive it and attach the conversion to the previously logged click.

Why “S2S” (server-to-server) instead of a plain browser callback? Because in the browser the JS call can be blocked (ITP, adblock, JS off), and attribution disappears. Server-to-server goes between two servers without involving the browser — it cannot be blocked client-side.

Minimal postback to our API:

POST https://api.tds.so/postback
Content-Type: application/json
Authorization: Bearer <your-API-token>

{
  "click_id": "k3p9x7q",
  "event": "purchase",
  "value": 4990,
  "currency": "USD"
}

The response is a JSON object with the stitching status:

{
  "status": "ok",
  "click_id": "k3p9x7q",
  "campaign": "spring-sale",
  "matched": true
}

click_id (cid)

The unique signature of a click — the key that links click and conversion. Ours is 10 characters from a URL-safe alphabet (a-z, A-Z, 0-9, excluding I/l/0/O for readability). Passed in the redirect query string as ?cid=k3p9x7q.

Your job on the landing: keep cid when the visitor arrives and pull it out at conversion time. Most reliable is localStorage:

// On the landing, on load
const cid = new URLSearchParams(location.search).get('cid');
if (cid) localStorage.setItem('tds_cid', cid);

// On conversion (e.g. after payment)
const storedCid = localStorage.getItem('tds_cid');
// send storedCid alongside the order to your backend
// your backend posts to our API with this click_id

Macros

Tokens in the postback URL template that get replaced with real values at fire time. Useful when you build the URL once and don't want to substitute values manually on your backend.

Full list of our macros:

  • {click_id} — click identifier
  • {event} — event type (purchase, lead, registration)
  • {value} — conversion value (in minor units, cents/kopeks)
  • {currency} — ISO 4217 currency code (USD, EUR, RUB)
  • {geo} — click country (ISO-3166: US, RU, DE)
  • {device} — device type (mobile, desktop, tablet)
  • {source} — UTM source passed on click
  • {campaign} — UTM campaign
  • {timestamp} — UNIX time of the click
  • {ip} — client IP address (last octet stripped for privacy)

You configure the URL template in the campaign settings, and we substitute values automatically:

https://your-crm.com/track?cid={click_id}&val={value}&geo={geo}
// on fire becomes:
https://your-crm.com/track?cid=k3p9x7q&val=4990&geo=US

Hash / signature

A signature of the postback that the receiving side uses to verify the request came from us, not from a random script that guessed the URL. Computed as HMAC-SHA256 of the body with a shared secret:

signature = hmac_sha256(
  request_body,
  secret_key
)

// passed in a header
X-TDS-Signature: 3f1a8b9c...

On the receiving side — recompute the signature with the same secret and compare. Match — postback is genuine. No match — reject. Without signatures, an attacker can flood you with fake conversions and trash attribution reporting.

What we see in practice

9 out of 10 integrations in the first month skip signature verification. “It's a private URL, who would guess?” is the typical line. In reality, a competitor noticing the URL pattern in your HTML will spray 50,000 fake conversions overnight and ruin a quarter's worth of attribution. Signature check is 20 lines of code.

End-to-end flow

To stitch the terms together, follow a click-to-postback path:

# Step 1. User clicks the link
GET https://tds.so/abc?utm_source=newsletter

# TDS logs the click, generates click_id=k3p9x7q, redirects:
HTTP/1.1 302 Found
Location: https://your-shop.com/promo?cid=k3p9x7q

# Step 2. On the landing your JS stores cid in localStorage:
localStorage.setItem('tds_cid', 'k3p9x7q');

# Step 3. The user pays. Your backend knows: order_id=12345,
# amount=49.90, cid=k3p9x7q (sent from the front along with the order)

# Step 4. Your backend posts to our API:
curl -X POST https://api.tds.so/postback \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "click_id": "k3p9x7q",
    "event": "purchase",
    "value": 4990,
    "currency": "USD"
  }'

# Step 5. We receive it, find the click, attach the conversion,
# respond 200 OK. The "newsletter" campaign dashboard now shows
# +1 order, $49.90.

Checklist when the postback does not arrive

80% of support tickets land on one of these. Walk through in order:

  1. cid lost on the landing. DevTools → Application → Local Storage. If tds_cid is there after clicking our link — JS works. If not — storage bug.
  2. cid did not reach the backend. Confirm the front actually passes tds_cid in the order payload. Most often forgotten.
  3. HTTP request never fires. Log the postback send on your backend. Often a wrong URL or firewall stops it before going out.
  4. Request fires but we return an error. Inspect our API response: HTTP code + body. 401 — bad token, 422 — invalid JSON or missing fields, 404 — click_id not found (retention is 90 days).
  5. Postback arrived but the conversion is invisible. Check the campaign filter in the dashboard — you may be looking at the wrong one. The conversion landed, just under a different campaign.

For debugging we have a “sandbox” mode: test postbacks get logged but do not affect campaign statistics. Enable it in the integration settings, and you'll see the full raw payload in the logs.

Conclusion

A postback is a server-to-server HTTP request that stitches click and conversion. The key is click_id. Macros simplify URL templating. The signature (HMAC-SHA256) protects against forgery. If a postback does not arrive — walk the five-point checklist above (cid, backend, HTTP, API response, dashboard filter) and 80% of the time the issue is found in 5 minutes.

Related: if you are wondering why a plain JS pixel in the browser is not enough — see where UTM gets lost. ITP, webview, and AMP are three reasons client-side attribution stopped being enough in 2026, and server-side via postback became the standard.