Skip to content
LinkProfit

Conversion Tracking for Short Links: Click IDs, Windows and Server Events

LinkProfit Team11 min read
  • conversions
  • analytics
  • attribution
  • link-shortener
On this page

Every link report has a natural stopping point, and for most teams it is the click. The dashboard says a link was opened four thousand times last month, broken down by country, device, browser and source, and that is where the analysis ends. But four thousand clicks describes what you spent, not what you earned. It is the cheap half of the funnel, and it is the half that looks best in a screenshot.

Conversion tracking closes the gap by carrying an identifier from the moment of the redirect into whatever system eventually records the order, then sending it back. The idea is easy to describe and easy to get wrong in ways that only surface months later, as revenue reports that do not reconcile with invoices. This article covers the identifier, the attribution window and why it is a business decision, the difference between a server event and a browser event, why money is an integer, how duplicates are handled, and what to do when the conversion happens in a system you do not control.

A Click Is a Cost, Not a Result

Click-only reporting persists because clicks are the easy measurement. A redirect happens on infrastructure you control, so counting it requires nobody's cooperation, while everything downstream happens somewhere else. Our guide to how link clicks are actually tracked covers what a single click event can contain, and the short version is that it knows the click happened and nothing about what followed.

That limitation has real consequences. Two placements can produce identical click volumes while one sends buyers and the other sends people who bounce in three seconds. A country breakdown that looks impressive on clicks can invert on revenue. Split tests decided on click-through rate routinely pick the variant with the more aggressive headline and the worse checkout completion. Until revenue is attached to the same dimensions, each of those judgements is a guess dressed as a metric.

Attaching it needs one thing: a value that survives the trip from redirect to order.

The Identifier Is the Whole Design

On every redirect the worker issues a token and does two things with it. It appends the token to the destination address under a parameter name you choose, lp_cid by default, and it writes the same value into a first-party cookie on your redirect domain, valid for 90 days.

Two copies exist because either can be lost. Destinations strip query parameters, sometimes deliberately for tidy URLs, sometimes as a side effect of a redirect on their end. Meanwhile a visitor who clicks today and returns three days later by typing the address has no parameter left but still carries the cookie. Neither copy is reliable alone; together they cover most realistic paths through a purchase.

The cookie being first-party is not a technicality. It is set by your own redirect domain, the one the visitor actually navigated to, which is precisely the category of cookie browsers have not been removing. This is why a custom domain stops being a branding preference and becomes measurement infrastructure: on a shared vendor domain the cookie belongs to the vendor.

What the token carries

The token is signed, and the signature covers more than the click. Workspace and partner identifiers are part of the signed payload, so a token issued on one client's link is rejected in another client's workspace exactly like a forgery. For anyone running links for multiple clients, that keeps one client's revenue out of another's report by construction rather than by a filter someone remembered to apply.

What the token contains is deliberately narrow: the click moment, the link, the split variant, the country, the device class, the traffic source, and a flag for suspicious traffic. No personal data. That list is also what makes the revenue column useful, because each of those fields becomes a dimension you can break revenue down by in analytics without a second join against anything.

The parameter name and the on/off switch live in the workspace settings, and changing either rewrites the cached configuration of every link immediately. One consequence worth planning around: when a plan does not include conversions the redirector stops issuing identifiers at all, and enabling it later does not retroactively create identifiers for clicks that already happened.

The Attribution Window Is a Business Decision

The window is the maximum age of a click that can still be credited for a conversion. It is a per-workspace setting, and it is the one number here that should be argued about rather than accepted as a default.

Set it too short and you throw away revenue you genuinely produced. Set it far too long and you credit links for purchases that would have happened anyway, which is worse than useless because it is confidently wrong. Choose by measuring the gap between first touch and purchase for orders you can already trace: an impulse buy closes in minutes, a considered consumer purchase takes days, a business purchase with an approval step takes weeks.

The failure mode this design avoids is silent rejection. A conversion arriving past the window is refused with attribution_expired, a distinct code from invalid_click_id. Those two conditions call for entirely different fixes, and an integration that receives one generic error for both will spend a week debugging the wrong thing. Log the code, count both separately, and treat a rising attribution_expired rate as a sign that your window no longer matches your sales cycle.

The two intake paths also have different ceilings. The cookie lives for 90 days, so a browser-side report depending on it cannot outlive that. A server-side report stores the identifier itself and is bounded only by the workspace window.

Two Ways In, One Set of Rules

Both paths run through the same service, so the window, the deduplication and the outbound delivery behave identically. What differs is what each path survives and what each one requires of you.

The server event

Your backend posts the identifier with a goal, an order identifier, an amount and a currency.

curl -X POST https://api.linkprofit.com/v1/conversions \
  -H "Authorization: Bearer lp_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-10241" \
  -d '{
    "click_id": "1.eyJ1aWQiOiJ...",
    "goal": "purchase",
    "order_id": "10241",
    "amount_cents": 4999,
    "currency": "usd"
  }'

The key needs the conversions:write scope, which means this path costs you something the browser path does not: a credential to issue, store and rotate. See API authentication for how scoped keys are managed, and keep this key on a server — a key with write access to your revenue data does not belong in a page.

What you get in exchange is a report no ad blocker, script blocker, tracking-protection setting or JavaScript error can suppress. It fires from your own infrastructure at the moment your own system agrees an order exists, so it reflects the truth after fraud checks rather than the optimistic moment a button was pressed.

The browser event

The worker serves a small script from your own redirect domain, and the thank-you page calls it.

<script src="https://go.example.com/cv.js" defer></script>
<script>
  window.lpConversion({ goal: 'purchase', orderId: '10241', amountCents: 4999, currency: 'usd' });
</script>

The script reads the identifier from the URL or the first-party cookie, so nothing has to be passed to it manually. There is no third-party host anywhere in the chain, which matters twice: it is a privacy property, and a white-label one, because your client's page loads nothing that names the platform underneath.

The cost is the usual cost of client-side measurement. Blockers, privacy modes, failed script loads and a visitor closing the tab half a second early all remove events, and the loss is not uniform. Technical and privacy-conscious audiences suppress far more of it than consumer audiences do, so a browser-only setup does not just undercount, it undercounts unevenly by segment.

Choosing between them

Use the server path wherever a server knows about the order, which is nearly everywhere money changes hands. Use the browser path for outcomes that only exist in the browser, or where you cannot add server code: a page you can only edit through a tag manager, a signup on a hosted platform, a client's landing page you do not own.

Running both for one outcome is safe because of deduplication, but only if both paths send the same order_id. Without that you are not adding coverage, you are double-counting revenue.

Goals themselves are lightweight. A goal is a named outcome such as purchase, signup or trial, it can carry a default value and currency for conversions that arrive without an amount, and a goal referenced before it exists is created on first use.

Money Is an Integer

Amounts are whole minimum units. 4999 means 49.99 in a two-decimal currency, and an amount of 49.99 is rejected with the field named rather than rounded silently.

This annoys people for about a day and then saves them for years. Binary floating point cannot represent most decimal fractions exactly, so the classic demonstration where adding two amounts produces a third with an unexpected trailing digit is not a curiosity, it is what happens to every sum in a revenue column at scale. Zero-decimal currencies also break any assumption that dividing by a hundred is universally correct.

Refusing the decimal at the boundary pushes the one honest conversion, from a human-readable price to minimum units, into exactly one place: your code, once, where you can test it.

Duplicates Are the Database's Problem

Deduplication happens as a single atomic insert, not a read followed by a write. Repeated deliveries of the same order_id create one conversion and return it marked as a duplicate.

The distinction sounds academic and is not. A check-then-insert has a gap between the two operations, and a webhook retried by an impatient sender lands two calls inside that gap. Both see no existing record and both write one. That is how a revenue report grows a phantom ten percent nobody can locate afterwards, because each individual record looks perfectly legitimate.

The practical rule is to pick an order identifier that is stable and unique in your system, then never change how you derive it. Your own order number is usually right; a timestamp or anything regenerated on retry is exactly wrong. Pair it with an Idempotency-Key header, as in the request above, and a failed call becomes something you repeat rather than investigate.

Suspicious traffic is labelled rather than hidden. The classifier's verdict travels inside the token, so a conversion attributed to a data centre or a proxy is marked as suspect when recorded and appears in reports as its own segment. Two hundred conversions, and two hundred with forty from one hosting range inside an hour, are different facts, and only one is worth paying for. That is where conversion data and traffic filtering stop being separate features.

When the Conversion Happens Somewhere Else

Most of the difficulty in practice is not in the API. It is that the moment worth measuring occurs inside a hosted store, a CRM or a billing provider, and your job is to get one string from the click into that system and back out again.

Hosted stores. If the platform allows custom order attributes or metadata, that is the clean route: read the identifier when the visitor lands, carry it through checkout in a hidden field, store it on the order, and post the conversion from your server when the order is confirmed rather than when the button is pressed. If the platform allows no custom fields but does allow a script on the confirmation page, use the browser path instead. Our ecommerce solutions page covers these choices in more detail.

CRM and sales-assisted deals. Capture the identifier as a hidden field on the lead form and store it on the record, then send the conversion when the deal is marked won. The trap here is time: a deal that closes in seven weeks needs a window that accommodates seven weeks, and if your CRM is the only place that knows the identifier, its retention and export behaviour become part of your attribution architecture. Consider sending a signup goal at lead capture and a purchase goal at close, so the top of the funnel is measured even when the bottom takes a quarter.

Billing and subscriptions. Let the billing provider's webhook be the trigger. When an invoice is paid, your server looks up the identifier stored against that customer and posts a conversion with the invoice identifier as the order identifier. Recurring charges then arrive naturally as separate conversions, and first payments stay distinguishable from renewals through the goal.

In all three shapes the pattern is identical: the external system needs to know nothing about links. It holds one opaque string and gives it back to you.

Where the numbers travel afterwards is deliberately conventional. A signed conversion.created event is emitted through the normal webhook machinery, and configured advertising integrations receive the conversion through a delivery queue with a documented backoff and a give-up mark instead of infinite retries. Plans can also cap accepted conversions per calendar month, counted in UTC, and passing that cap answers quota_exceeded rather than a plan restriction, so "not in your plan" and "used up this month" are distinguishable without a support ticket.

Getting It Right the First Time

  1. Enable conversions before the campaign, not after. Identifiers are issued at redirect time and cannot be created retroactively.
  2. Run links on your own domain so the first-party cookie belongs to you.
  3. Set the window from your measured buying cycle, then revisit it when the attribution_expired rate moves.
  4. Prefer the server path wherever a server knows about the order.
  5. Convert prices to whole minimum units in exactly one place in your code.
  6. Use your real order number as the order identifier, and send it identically from every path.
  7. Log rejection codes separately, then reconcile against billing once per period and investigate the gap rather than averaging it away.

The full field reference lives in the conversions documentation, and the conversion tracking feature page covers how the revenue column behaves across the reports you already read. None of this replaces accounting. It answers a narrower question accounting cannot: which link, which country, which destination and which traffic source produced the money.

Questions people ask

What exactly is a click identifier and where does it live?

It is a signed token issued by the redirector at the moment of the redirect. It is appended to the destination address under a parameter name you choose, defaulting to lp_cid, and the same value is written to a first-party cookie on your own redirect domain with a 90-day lifetime. Two copies exist because either one can be lost: a destination that strips query parameters still has the cookie, and a visitor who returns days later with a cleared query string still carries it. The token is signed and bound to the workspace and partner it was issued for, so a token from one client's link is rejected in another client's workspace exactly like a forgery.

Does conversion tracking work without third-party cookies?

Yes, because nothing in the chain is third-party. The identifier travels in the destination URL, and the backup cookie is set by your own redirect domain rather than by a platform host. The browser script that reports conversions from a thank-you page is also served from your redirect domain, so the page loads nothing that belongs to another company. The mechanism browsers have been removing is the cookie set by a host the visitor never navigated to, and that mechanism is not used here.

Why are amounts sent in whole cents instead of decimals?

Because a rounding rule invented in the middle of a payment path is how revenue reports quietly stop matching invoices. Amounts are whole minimum units: cents, pence, kopecks. An amount of 49.99 is rejected with the offending field named rather than silently rounded to something plausible. Floating-point arithmetic cannot represent most decimal fractions exactly, so summing thousands of them drifts, and the drift is invisible until finance asks why the dashboard and the ledger differ by a few hundred units.

What happens if my system reports the same order twice?

One conversion is recorded and the second call returns that same conversion marked as a duplicate. Deduplication is a single atomic insert rather than a read followed by a write, which means it still holds when two deliveries arrive at the same instant from a webhook that was retried. This is what makes retrying safe: an integration that cannot tell whether its previous call succeeded should simply send again with the same order identifier.

How should I choose an attribution window?

By measuring how long your buying cycle actually takes, not by copying a number from an advertising platform. Look at the gap between first touch and purchase for real orders: an impulse purchase closes in minutes, a considered one takes a week, an enterprise deal takes a quarter. A window shorter than your real cycle throws away revenue you earned; a window much longer credits links for purchases they had nothing to do with. A conversion that arrives after the window is rejected with attribution_expired, a distinct code from invalid_click_id, so your integration can tell 'too late' from 'broken identifier' without guessing.

Can I attribute a conversion that happens in a store or CRM I do not control?

Usually yes, provided the system lets you store one extra string on the order or the record. Capture the identifier from the URL or the first-party cookie when the visitor arrives, put it in a hidden form field or a custom order attribute, and post it back from your server when the order is confirmed. If the platform allows no custom fields at all but does allow a script on the confirmation page, the browser path works instead. The one case with no clean answer is a system that neither accepts custom data nor allows scripts, where the honest options are a rebuilt checkout or per-link landing pages.