Affiliate tracking without third-party cookies: 8 steps for Stripe SaaS
Google kept third-party cookies in Chrome while Safari, Firefox and Brave restrict them. Build first-party affiliate tracking in 8 steps with Stripe and email.
Muzahid Maruf, FounderUpdated
On this page
- 01Which browsers block third-party cookies in 2026?
- 02Why do third-party cookies fail affiliate tracking, and what replaces them?
- 03How do you set up affiliate tracking without third-party cookies in 8 steps?
- 04What happens when a trial pays on day 16?
- 05What does a first-party setup still miss?
- 06What does TrackRev do for first-party affiliate tracking?
Explore with AI
Opens this article inside the chosen assistant with a ready-made prompt.
Affiliate tracking without third-party cookies keeps the proof of each sale on systems you control: a click log on your own domain, a visitor ID in a first-party cookie, and that ID on the Stripe payment.
A server matches payment to click and credits the partner. Chrome, at 66.36% of worldwide usage, still allows third-party cookies, but Safari has blocked them since March 24, 2020 and Firefox has partitioned them since version 103.
The method has 4 parts: a redirect that logs the click, a visitor ID, a payment reference and a server job that matches them.
According to Statcounter, Safari holds 18.27% of worldwide usage, so the cookie rules reach a large share of buyers.
Key takeaways
- No Chrome removal date exists after Google's two 2025 announcements. Safari's block dates from 2020 and Firefox's partitioning from version 103, so those buyers are the ones a cookie-dependent tracker loses.
- Statcounter's September 2026 data gives Safari 18.27% of worldwide browser usage and 46.38% of US mobile.
- Four pieces do the work: a redirect that logs the click, a visitor ID, a payment reference and a server job that matches them.
- Stripe takes the ID in client_reference_id (200 characters) or metadata (50 keys, 500-character values).
- Save the visitor ID on the user row at signup and read it from there at checkout, so a Safari visitor whose script cookie was deleted after 7 days still resolves.
Which browsers block third-party cookies in 2026?
| Date | Browser (share) | What changed |
|---|---|---|
| Feb 21, 2019 | Safari (18.27%) | Intelligent Tracking Prevention 2.1 caps persistent cookies created through document.cookie at 7 days (Safari 12.1) |
| Mar 24, 2020 | Safari (18.27%) | Safari 13.1 blocks cookies for every cross-site resource by default |
| Firefox 103 | Firefox (2.8%) | State partitioning is on by default for all users, so Firefox stores a third-party cookie separately for each top-level site |
| Default setting | Brave (not reported) | Shields block cross-site cookie tracking |
| Apr 22, 2025 | Chrome (66.36%) | Google will not roll out a standalone prompt for third-party cookies; users choose in settings, and Incognito windows block them |
| Oct 17, 2025 | Chrome (66.36%) | Chrome maintains its current approach to cookie choice, and 10 Privacy Sandbox technologies are retired, including the Attribution Reporting API |
Shares are Statcounter's worldwide figures for all platforms, September 2026.
No Chrome cutoff is coming, which leaves Safari as the largest of the blockers. Statcounter's US mobile data for September 2026 puts Safari at 46.38%, 1.06 points behind Chrome's 47.44%, with Samsung Internet at 3.28% and Firefox at 1.29%.
Worldwide across all platforms, Edge holds 6.04% and Samsung Internet 2.31%. Split paying customers by browser to find your own mix. Safari and Firefox together hold 21.07% of worldwide usage, so blocking affects about 1 visitor in 5.
Third-party cookie blocking means a browser refuses cookies set by a site other than the one the visitor is on.
| Browser | Worldwide, all platforms | US mobile |
|---|---|---|
| Chrome | 66.36% | 47.44% |
| Safari | 18.27% | 46.38% |
| Edge | 6.04% | Not listed |
| Firefox | 2.8% | 1.29% |
| Samsung Internet | 2.31% | 3.28% |
Statcounter, September 2026.
Why do third-party cookies fail affiliate tracking, and what replaces them?
A network-style link such as network.example/click?aff=123 sets a cookie on network.example and redirects to your site. A pixel on your confirmation page then fires at network.example, which matches the sale only if the browser attaches that cookie.
Safari 13.1 blocks that cookie by default. Firefox 103 looks only in the jar partitioned under your site, which is empty. Content blockers can drop the pixel request too.
WebKit's tracking prevention page says ITP deletes script-created cookies after 7 days without user interaction and caps response cookies at 7 days for third-party CNAME or IP address cloaking.
Nothing on that page limits a cookie your server sets on your own domain, so the redirect carries the identity. The Safari ITP post has the full rule table.
Per WebKit's page, checked in 2026, no rule limits a server cookie on your own domain.
How do you set up affiliate tracking without third-party cookies in 8 steps?
| Step | Where the ID lives | Number to remember |
|---|---|---|
| 2 | Click row and a first-party cookie | Max-Age=31536000, 365 days |
| 3 | Cookie on the registrable domain | 7-day Safari cap on script copies |
| 4 | User row beside the email | Still present on day 9 in the example |
| 5 | Stripe Checkout Session | 200 characters, and a UUID uses 36 |
| 6 | Click lookup by ID, then by email | 45-day lookback |
| 7 | Coupon code on the invoice | 1 unique code per partner |
| 8 | Three test browsers | Safari, Firefox, Chrome Incognito |
Steps 1 to 8 at a glance. Step 1 is the partner link itself.
Step 1: how do you put partner links on a domain you control?
A first-party link is a partner URL served from a domain the merchant owns. Give each partner a link such as go.example.com/anna, backed by a redirect you run.
If the host CNAMEs to a vendor, read the cookie's expiry in Safari after a test click, because WebKit's Intelligent Tracking Prevention caps response cookies at 7 days when it detects CNAME cloaking.
A 7-day cap on a 60-day cookie window cuts the window by about 88%. A branded subdomain also lets you change vendors later without asking partners to rewrite links, because every link already carries your own name.
Step 2: how do you log the click and mint a visitor ID?
The redirect is the component that records each click before the visitor reaches the site. The redirect writes one click row and skips bots and link-preview fetchers.
A 302 is a temporary redirect, and the response carries both the destination with _vid on the URL and the ID in a 365-day cookie.
One row per click gives the later match a timestamp, a partner and a user agent to work with.
According to WebKit's tracking prevention page, the 7-day rule targets JavaScript-written cookies, so the 365-day cookie from the redirect is not on that list. The route below stores five fields and returns the redirect in a single response.
// GET go.example.com/:partnerexport async function GET(req: Request, { params }: { params: { partner: string } }) { const ua = req.headers.get("user-agent") ?? ""; const existing = /(?:^|; )vid=([^;]+)/.exec(req.headers.get("cookie") ?? "")?.[1]; const vid = existing ?? crypto.randomUUID(); // Partner, timestamp, user agent, referrer and visitor ID in one row if (!isBot(ua)) { await db.clicks.insert({ vid, partner: params.partner, userAgent: ua, referrer: req.headers.get("referer"), at: new Date(), }); } const dest = new URL("https://example.com/pricing"); dest.searchParams.set("_vid", vid); return new Response(null, { status: 302, headers: { Location: dest.toString(), "Set-Cookie": "vid=" + vid + "; Max-Age=31536000; Path=/; Domain=example.com; Secure; SameSite=Lax", }, });}Step 3: how do you keep the ID on your own site?
A registrable domain is the part of a hostname a site owner can register, such as example.com. A cookie set with Domain=example.com works on every subdomain, so go.example.com and app.example.com share one visitor ID.
If the redirect shares your registrable domain, step 2's cookie is already on your site.
A redirect on another domain hands over only _vid, so read it on the landing page and write the cookie yourself; Safari deletes script-written cookies after 7 days without interaction.
Check each hop to the signup form (http to https, a trailing slash, an /en/ prefix) for a dropped query string.
const vid = new URLSearchParams(location.search).get("_vid");
if (vid && !/(?:^|; )vid=/.test(document.cookie)) {
document.cookie = "vid=" + vid + "; Max-Age=31536000; Path=/; Domain=example.com; Secure; SameSite=Lax";
}Step 4: how do you bind the ID to an email at signup?
A signup bind is the step that copies the visitor ID onto the user record. Store the visitor ID on the user row beside the email, since a script-written cookie can disappear after 7 days.
An email survives a deleted cookie, a new browser and a second device, and the email sits on the Stripe customer too. The bind costs one extra column and one line in the signup handler, shown below.
In the day-16 example later in the post, the script cookie is gone on day 9, 7 days before payment, and the user row still holds the ID.
const vid = req.cookies.get("vid")?.value ?? null;
await db.users.insert({ email, vid, createdAt: new Date() });Step 5: how do you write the ID onto the payment?
Writing the ID onto the payment is the step that lets Stripe return the visitor ID with the webhook. A UUID uses 36 characters, 18% of the 200-character limit on client_reference_id.
Read the ID from the user row when you create the Stripe Checkout Session in your backend. A visitor whose cookie vanished on day 9 then still pays on day 16 with the ID attached.
A Payment Link, Buy Button or pricing table has no backend step, so the cookie value goes into its URL parameter or attribute and the email route covers the rest.
| Checkout path | Where the ID goes | Limit |
|---|---|---|
| Checkout Session from your backend | client_reference_id | 200 characters |
| Subscription created by Checkout | subscription_data.metadata | 50 keys, keys to 40 characters, values to 500 |
| Payment Link | client_reference_id URL parameter | Alphanumeric, hyphen and _ characters, up to 200 |
| Buy Button or pricing table | client-reference-id attribute | Same characters, up to 200 |
| PaymentIntent you create yourself | metadata on the PaymentIntent | Same 50, 40 and 500 limits, copied once to the Charge |
Limits from Stripe's Checkout Sessions, Payment Links, Buy Button and pricing table documentation.
A UUID is 36 characters, inside every limit above. Stripe's metadata documentation lists a few exceptions and otherwise says metadata does not copy between objects, so session metadata reaches the Subscription only through subscription_data.metadata.
The Payment Links page covers the URL parameter, the Buy Button and pricing table pages cover the attribute, and the Stripe metadata attribution setup post maps every field.
const user = await db.users.find(currentUserId); const checkout = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: "price_123", quantity: 1 }], customer_email: user.email, client_reference_id: user.vid, subscription_data: { metadata: { vid: user.vid } }, success_url: "https://example.com/welcome", cancel_url: "https://example.com/pricing",});Step 6: how do you match payments to clicks on the server?
A matching job is a server routine that joins each payment to a click. Listen for checkout.session.completed, which carries the session's client_reference_id and metadata.
Look up the click by visitor ID, then by the customer's email, within 45 days of the payment (a 14-day trial plus 30 days to decide, rounded up). Copy the credit onto each renewal.
The credit rule matters when a visitor clicked 3 partners, say A on day 1, B on day 10 and C on day 20, then paid $79 on day 21 (the attribution models comparison covers the trade-offs):
| Rule | Credited | Commission at 20% |
|---|---|---|
| Last click | Partner C, $79 | $15.80 to C |
| First click | Partner A, $79 | $15.80 to A |
| Shared | A, B and C, $26.33 each | $5.27 each |
if (event.type === "checkout.session.completed") { const s = event.data.object; const vid = s.client_reference_id ?? s.metadata?.vid ?? null; const click = (vid && (await db.clicks.latestForVisitor(vid, { withinDays: 45 }))) || (await db.clicks.latestForEmail(s.customer_details?.email, { withinDays: 45 })); if (click) { await db.commissions.createOnce({ sessionId: s.id, partner: click.partner }); }}Step 7: how do you cover sales that have no click?
Give podcast, YouTube and Instagram partners a personal coupon code and credit the sale when it shows on the invoice, as the coupon code tracking guide describes. A code shared by all partners cannot tell them apart.
A coupon fallback is the credit rule for sales that arrive without any recorded click, such as a podcast listener who types a code at checkout.
Unique codes also expose abuse, because a code posted on a coupon site appears on invoices with no partner click behind it.
Step 8: how do you test in Safari, Firefox and a private window?
A test run is a proof of the whole chain, not one component. Safari on an iPhone exercises the 7-day script-cookie rule, Firefox exercises state partitioning, and a private window exercises the no-cookie path.
In Stripe test mode, click a partner link in Safari on an iPhone, sign up, delete the site's cookies and pay. The click row, user row and payment should carry one visitor ID.
Repeat in Firefox and Chrome Incognito, then pay once with the ID stripped to prove the email route.
What happens when a trial pays on day 16?
The numbers here are invented for illustration. A visitor clicks a partner link in Safari on day 0. The redirect runs on a short domain, so the landing page writes the cookie with JavaScript.
The visitor signs up on day 2 for a 14-day trial and pays $79 on day 16, and the partner earns 20% recurring.
| Day | Event | Script cookie in Safari | What the match uses |
|---|---|---|---|
| 0 | Click on the partner link | None yet | Click row and visitor ID from the redirect |
| 2 | Signup, trial starts | Written | Visitor ID saved on the user row with the email |
| 9 | 7 days without a visit | Deleted | Nothing changes; the user row still holds the ID |
| 16 | Trial converts, $79 charge | Absent | ID read from the user row into Stripe; email as fallback |
At 20%, the first payment earns the partner $15.80 ($79 × 0.20). A customer who stays 12 months earns 12 × $15.80 = $189.60.
Without the signup bind, the day-16 payment arrives with no source and the partner loses that whole stream, since every renewal inherits the first payment's credit.
| Months the customer stays | Revenue at $79 | Partner earns at 20% |
|---|---|---|
| 1 | $79 | $15.80 |
| 3 | $237 | $47.40 |
| 6 | $474 | $94.80 |
| 12 | $948 | $189.60 |
| 24 | $1,896 | $379.20 |
| 36 | $2,844 | $568.80 |
What the signup bind protects, using the same invented figures.
What does a first-party setup still miss?
A first-party setup is a strong default but not a complete one. The table lists 5 gaps and the step that covers each, for example a $79 purchase made on a laptop after a click on a phone.
Cross-device purchases fall to the email match in step 4, bare-URL shares fall to coupon codes in step 7, and consent rules sit outside the engineering fix. Review the gaps every quarter against your real refund and direct-sale counts.
The first-party link tracking post covers the cookie-free variant, and the affiliate tracking explainer maps the same chain.
| Gap | Why it happens | Covered by |
|---|---|---|
| Click on a phone, purchase on a laptop | Each browser holds its own cookie | Step 4 |
| Partner shares the bare site URL | No click reaches your redirect | Step 7 |
| A redirect drops the query string | The landing page never sees _vid | Step 3 |
| A content blocker stops your script | No script cookie and no pageview | Steps 2 and 4 |
| Visitors in regions that require consent | The UK ICO says online advertising storage and cross-site tracking need consent | Counsel, plus the FTC and GDPR guide |
What does TrackRev do for first-party affiliate tracking?
TrackRev is SaaS affiliate software for indie hackers and small SaaS teams. TrackRev covers the click log, the visitor ID and the payment match; you add the p.js pixel and one trk.identify(email) call at signup.
Each partner link in the redirect we built is a server-side redirect that logs the click and sends the visitor to your page with the ID in _vid, on a TrackRev-hosted subdomain, a custom domain with 1 CNAME record, or the shared short domain.
At payment, TrackRev checks 4 places in our lookup order:
| Order | Where TrackRev looks |
|---|---|
| 1 | Charge metadata, key vid |
| 2 | Checkout Session client_reference_id |
| 3 | Checkout Session metadata, key vid |
| 4 | The customer's email, matched to the visitor bound by identify |
The pixel adds the ID to Stripe Payment Links, Buy Buttons and pricing tables itself.
One workspace-wide rule sets credit (last click, first click or shared) over a lookback window of 1 to 365 days, and renewals copy the first sale's credit.
Coupon-code commissions run alongside, a refund reverses its commission, and each program can hold commission for 0 to 180 days, with 0 the default in our own settings. With data-requires-consent="true" the pixel writes no cookie until your code calls window.trk.grantConsent().
Partner programs need a paid plan, and in our pricing the entry point is Indie.
| Plan | Price | Workspaces | Partner program | Commission tracked |
|---|---|---|---|---|
| Free | $0 | 1 | Locked; link tracking only, 50 links and 1,000 events | None |
| Indie | $29 a month | 1 | Unlocked | No cap |
| Growth | $299 a year | Up to 10 | Unlocked | No cap |
From the pricing page; the affiliate program page covers links and payouts. Indie bills monthly ($348 over 12 months) and Growth bills yearly ($299, or about $25 a month).
Skip the switch if your current tool already writes a first-party cookie and sends the referral ID to Stripe; step 8 will tell you.
A network whose contract makes its pixel the record for disputes keeps that pixel, with first-party logging beside it. TrackRev reads revenue from Stripe, Paddle Billing, Polar, Lemon Squeezy, Creem and Dodo Payments.
Found this useful? Share it.
Frequently asked questions
- No. Chrome allows them unless the person blocks them or browses in Incognito, and nothing Google announced in 2025 changed that.
- Only if their links point at a network's click URL. Map old deep links to the new format with 301 redirects.
- It depends on your jurisdiction and the cookie's purpose, so ask counsel before launch.
- Count the trial plus your partners' usual decision time, then round up: 14 and 30 make 44, so 45. Longer windows credit more clicks that had no effect on the sale.
- Use client_reference_id (200 characters) on Checkout Sessions and Payment Links, plus subscription_data.metadata (50 keys) so the Subscription carries it too.
- Not one set in a response from your own domain. The 7-day deletion targets JavaScript-written cookies, and the response-cookie cap covers cloaked third-party hosts.

Written by
Founder, TrackRev.io & Contant.io
Muzahid Maruf founded TrackRev.io, SaaS affiliate software with no limit on tracked revenue, and Contant.io. He writes about affiliate programs.
Writes about Marketing attribution · Link tracking · Revenue analytics · SaaS growth
Run your affiliate program without a revenue cap.
Affiliate software for indie hackers and small SaaS teams: $29 a month flat, 0% fees, link tracking and revenue attribution built in. Free to start, no card.