You buy the traffic, the sale happens on a domain you cannot touch. An affiliate offer, a product page on a marketplace, a partner’s checkout, any program where the “thank you” page belongs to the platform. You cannot place a pixel there, and the ad platform is optimizing blind: the network dashboard shows dozens of conversions, the ad account registers a handful, and the algorithm steers toward clicks, because clicks are all it can see.
The question surfaces in PPC and affiliate communities year after year, asked about Meta traffic to marketplaces, about ClickBank offers, about white label programs, and the public answers have not moved in a decade. They come in two kinds: “you cannot implement anything on their side”, which is true, and “buy a tracker”, which starts at three figures a month. At least one asker gave up and publicly put a €100 bounty on any working idea. Nobody collected.
The fallback everyone lands on instead is counting clicks on the outbound link as stand in conversions. It rots, because a click is not a sale and bots click too — that €100 was in fact offered for a way to filter bots out of exactly this surrogate metric. Money on the symptom, because the cure was not written down anywhere.
The “nothing on their side” answer is correct, and it is also not the end. Everything you need can live on yours, in the one place every click still passes through: a redirect hop you own. That pattern is what this article builds — with what it costs, where it breaks, and what the ad platforms allow, checked against their live documentation on 27 July 2026.
The loop: click_id out, postback in
The mechanics have been standard in affiliate networks for a decade. What is missing in every one of those threads is someone laying the loop out end to end.
- The click leaves your page through a redirect you host:
/go. The hop generates aclick_id, stores it alongside everything you know about the click — thegclidfrom the ad, the source, the timestamp — and sends a302to the network link with theclick_idin the network’s subid parameter. - The network carries your subid through to the sale. When the conversion happens on their side, their system fires a postback: a server to server request to a URL you registered, carrying the subid back, usually with a payout amount.
- Your postback endpoint looks the
click_idup, and now the conversion is joined to the exact click, campaign, and creative that produced it — on your infrastructure, from server logs, with no pixel anywhere.
Note what this does not depend on. No JavaScript on the conversion page, because you have no JavaScript there. No cookies surviving anything, because the join key travels inside URLs and server calls. No consent banner in the path, because nothing runs in the visitor’s browser beyond the redirect itself. The hop is server side by construction, which is also why ad blockers never see it: the click is a request to your own host before the browser goes anywhere else.
Why the hop must not be your ad’s final URL
The tempting shortcut is to point the ad directly at /go and skip the landing page.
On Google Ads that shortcut is closed twice over, and knowing exactly how saves you a
policy strike.
First, tracking templates do not carry the user anymore. Parallel tracking is mandatory for Search, Shopping, Display, Video, and Performance Max, and Google’s description of it is unambiguous:
Parallel tracking sends customers directly from your ad to your final URL while click measurement happens in the background (without sending them to the tracking URLs first).
The template URL still gets requested — as a background ping from the browser — so a hop in the tracking template can count the click. But it cannot route the visitor, rewrite where they land, or hand the network a subid on the visit that actually converts.
Second, making the hop the final URL is a listed policy violation. Google’s destination requirements name it directly: “Redirects from the final URL that take the user to a different domain” is a destination mismatch. That is the rule that gets affiliate accounts suspended for direct linking.
So the compliant shape on Google Ads is a bridge: the final URL is a real page on your
domain, and the hop is the outbound link on that page. Make sure the page passes its own
query string through to the hop link — the gclid arrives on the landing page URL thanks
to auto tagging, and a redirect that drops it
ends attribution before it starts. Meta does not run parallel tracking, but the same
bridge shape is where community answers converge for Meta traffic too: a page you own,
your pixel on it, subids outbound.
Closing the loop back into the ad platform is the step everyone skips. On Google Ads it
has an official name: offline conversion import. You stored the gclid next to the
click_id at hop time; when the postback lands, you upload the conversion keyed by that
gclid. Google retains a gclid for 90 days for this purpose, auto tagging is the only
prerequisite, and the feature is current, not legacy. The campaign stops optimizing
toward clicks and starts optimizing toward the sales your postbacks confirm.
Building the hop on Cloudflare’s free plan
A redirect rule cannot do this job, and it is worth being precise about why. Single
Redirects are a terminating action in the first request phase —
nothing downstream ever sees the click, and the rule
itself cannot write anything anywhere. The expression language cannot even mint an id:
uuidv4() exists, but the documentation restricts it to rewrite expressions of Transform
Rules. Among every way to redirect on Cloudflare,
the only primitive that can mint, store, and answer postbacks is a Worker.
The whole hop is one Worker with two routes and a D1 table:
export default {
async fetch(req, env) {
const url = new URL(req.url);
if (url.pathname === '/go') {
const id = crypto.randomUUID();
await env.DB.prepare(
'INSERT INTO clicks (id, gclid, src, ts) VALUES (?1, ?2, ?3, ?4)',
)
.bind(id, url.searchParams.get('gclid') ?? '', url.searchParams.get('src') ?? '', Date.now())
.run();
const target = new URL(env.OFFER_URL);
target.searchParams.set('subid', id);
return Response.redirect(target.href, 302);
}
if (url.pathname === '/postback') {
if (url.searchParams.get('key') !== env.POSTBACK_KEY) {
return new Response(null, { status: 403 });
}
await env.DB.prepare(
'UPDATE clicks SET payout = ?2, converted = ?3 WHERE id = ?1',
)
.bind(url.searchParams.get('subid'), url.searchParams.get('payout') ?? '', Date.now())
.run();
return new Response('ok');
}
return new Response(null, { status: 404 });
},
};
The free plan quotas fit this comfortably, all checked live on 27 July 2026: Workers
allows 100,000 requests a day, D1 writes 100,000 rows a day and reads five million — a
click and a postback are one row each. The one storage choice to get right: this is a
lookup workload, not a counting workload, which is why the table is D1 and not Workers
KV — KV writes cap at 1,000 a day on Free, a limit that
has misled people before. Registering the postback URL
with the network is one form field on their side: your /postback address with the key
and their subid macro in it.
Where it breaks
The hop is the reliable half of the loop. The other half belongs to the network, and when the join fails, it fails in one of five recurring ways — this list is assembled from a decade of affiliate forum threads asking why the postback never came:
| Failure | What it looks like | Check |
|---|---|---|
| Token name mismatch | Network expects {clickid}, you sent {click_id} |
Their macro list, character for character |
| Reserved subids | sub1 is taken by the network’s own tracking |
Ask which slot is passthrough |
| Offer strips subids | Clicks arrive, subid column is empty | Test link, then check their reporting |
| Postback registered late | Conversions before registration are gone for good | Register before the first paid click |
| Never tested by hand | Everything “configured”, nothing verified | Open your postback URL in a browser with a fake subid |
The last row is the one that catches people who did everything else right. A postback URL you have never fired manually is a guess, not a setup: request it once yourself and watch the row update before any money is spent.
Two structural limits are worth naming too. If the program offers no postback and no subid
reporting at all, no hop can conjure the loop — the join needs their half, and your
fallback is reconciling their dashboard against your click log by time and creative,
which is exactly the manual matching those community threads end up settling for. And the
offline
conversion window is real: a sale confirmed more than 90 days after the click cannot be
imported against its gclid, which matters for products with long approval cycles.
When the Worker is enough, and when it stops being
For one offer and one traffic source, the Worker above genuinely closes the €100 question:
every click logged with its gclid, every conversion joined server side, the ad platform
fed real sales instead of proxy clicks. Run it and stop paying for attribution you can
build in an afternoon.
The pattern stops being an afternoon when it multiplies. Ten offers mean ten target URLs
and ten postback registrations; three traffic sources mean the hop has to split by
source; a dead offer means rerouting yesterday’s links without breaking yesterday’s
subids. At that point the hop is not a script anymore, it is a routing layer with a
database behind it — which is the thing 301.st already is: streams that
route each click by rules, click_id minted and logged on every hop, postbacks received
and matched as the default path, not as your weekend project. For a single campaign, keep
the Worker. It is honest work and it is yours.