SaaS / Next.js · October 2026 · 6 min read
How to Add a Referral Program to Your Next.js App (2026)
The actual 30-minute integration. Capture ?ref= on landing, pass through Stripe customer metadata, attribute conversions server-side from your Stripe webhook. Code examples for App Router, Pages Router, and the boring stuff that nobody documents.
If you're shipping a SaaS, a Next.js storefront, or anything in between and you're considering "should we add a referral program?" — the answer is almost always yes once you've got product-market fit. Affiliate-driven revenue compounds, the CPA is bounded by your commission rate, and it's the cheapest paid acquisition channel that exists for most ecommerce / SaaS businesses (see our breakdown vs influencer marketing).
The hard part has always been the engineering. Existing tools like Rewardful, FirstPromoter and Tapfiliate work but cost $49–$200/mo, take a percentage cut of attributed revenue, and assume you'll learn their proprietary SDK. This guide walks through a 30-minute integration that gives you a working program with one POST endpoint and a tiny piece of client-side code. We'll use AffRef as the example because that's what we make — but the same pattern works against any tracking platform's API.
What you're building
- Affiliate shares a link:
https://yourapp.com?ref=SARAH - Shopper clicks. Your Next.js app reads
?ref=from the URL and stores it in a first-party cookie on your domain. Survives Safari ITP, ad blockers, the lot. - Shopper signs up + pays. At Stripe Checkout creation you pull the cookie value and inject it into
subscription_data.metadata.affiliate_code. - Your existing Stripe webhook handler reads the metadata and POSTs a conversion to your referral platform. Affiliate gets credited.
Four moving parts. Each is a few lines of code. Let's go.
Prerequisites
- A Next.js app (App Router or Pages Router both work).
- Stripe-based billing (subscriptions or one-time payments).
- A referral platform with an HTTP API. AffRef has a 7-day free trial; you'll need an API key.
- An existing Stripe webhook handler — almost every SaaS already has one for
customer.subscription.createdorcheckout.session.completed.
Step 1 — Capture ?ref= on landing
Drop this into your root layout so it runs on every page hit:
// app/layout.tsx (App Router)
'use client';
import { useEffect } from 'react';
function CaptureReferral() {
useEffect(() => {
const ref = new URL(window.location.href).searchParams.get('ref');
if (ref) {
document.cookie = `affref=${encodeURIComponent(ref)}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
}
}, []);
return null;
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<CaptureReferral />
{children}
</body>
</html>
);
}
For Pages Router, do the same in _app.tsx. The cookie has SameSite=Lax so it survives most browser privacy restrictions, and it's first-party (on your domain) so Safari ITP doesn't touch it.
Cookie duration is 30 days here. Match this to your platform's attribution window. 30 days is standard; bump to 60–90 if you're in a category with longer buying cycles.
Step 2 — Pass the affiliate code into Stripe metadata at checkout
When the user actually hits "Subscribe" or "Buy", your server creates the Stripe Checkout Session. Read the cookie server-side and stash the code in the subscription's metadata:
// app/api/checkout/route.ts (App Router)
import { cookies } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const cookieStore = await cookies();
const affiliateCode = cookieStore.get('affref')?.value;
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: 'price_xxx', quantity: 1 }],
success_url: 'https://yourapp.com/welcome?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://yourapp.com/pricing',
subscription_data: {
metadata: {
affiliate_code: affiliateCode ?? '',
},
},
metadata: {
affiliate_code: affiliateCode ?? '',
},
});
return Response.json({ url: session.url });
}
Setting metadata on both the session and the subscription_data is important — they're separate objects and you'll read from the subscription later. Skipping subscription_data.metadata is the most common bug.
Step 3 — Fire the conversion from your Stripe webhook
You already have a Stripe webhook handler. Add a few lines to fire a conversion to AffRef when a subscription is created:
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const sig = request.headers.get('stripe-signature')!;
const body = await request.text();
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
if (event.type === 'customer.subscription.created') {
const sub = event.data.object as Stripe.Subscription;
const affiliateCode = sub.metadata.affiliate_code;
if (affiliateCode) {
await fetch('https://app.affref.com/api/v1/conversions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.AFFREF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
affiliate_code: affiliateCode,
order_id: sub.id,
order_total: (sub.items.data[0].price.unit_amount ?? 0) / 100,
currency: sub.currency.toUpperCase(),
customer_email: typeof sub.customer === 'string'
? undefined
: (sub.customer as Stripe.Customer).email ?? undefined,
}),
});
}
}
return new Response('ok', { status: 200 });
}
Idempotency is handled server-side — AffRef's conversion endpoint is idempotent on order_id, so if Stripe retries the webhook (which it does) you don't get duplicate conversions.
Step 4 — Create some affiliates and test
In your AffRef dashboard, create an affiliate, copy their code (e.g. SARAH), then test end-to-end:
- Open incognito Chrome / Safari (avoid existing cookies).
- Visit
https://yourapp.com?ref=SARAH - Open DevTools → Application → Cookies. Confirm the
affrefcookie is set toSARAH. - Complete a real signup + payment (use a test card or your own real card; you can refund yourself after).
- Within ~30 seconds, the conversion should appear in your AffRef dashboard attributed to Sarah.
If it doesn't show: 99% of the time it's missing subscription_data.metadata at checkout creation (step 2). Check the actual subscription object in your Stripe dashboard and confirm the metadata key is present.
Things that will trip you up
Cookie SameSite + cross-domain Stripe Checkout
Stripe Checkout runs on checkout.stripe.com, not your domain. Your cookie doesn't travel there. That's why we pass the code via session metadata — the cookie is read server-side before the redirect and serialised into the Stripe session.
Multiple subscriptions per customer
If your SaaS upgrades / downgrades cancel-and-recreate the subscription, only the first one counts as "attributed." If you do additive subscriptions instead, you can fire a conversion on each. Decide which model fits your business and don't double-attribute.
Refunds
Add a handler for customer.subscription.deleted and charge.refunded that calls your platform's reverse-commission endpoint. Otherwise you'll pay affiliates on customers who churned in the trial.
Tracking on the marketing site vs the app
If your marketing site and your app are on different subdomains (www.yourapp.com vs app.yourapp.com), set the cookie's domain to the parent: document.cookie = `affref=...; domain=.yourapp.com; ...`. Otherwise it'll be locked to whichever subdomain set it.
Why this pattern is better than installing an SDK
- No vendor lock-in. You wrote 30 lines of code. If your tracking provider gets acquired and goes bad, you can swap providers by changing two URLs.
- No client-side bloat. Most affiliate SDKs ship hundreds of KB of JS to your users. This is one cookie write.
- No analytics privacy headaches. First-party cookie, no third-party calls from the client.
- Testable. The whole flow is HTTP requests you can unit-test or replay.
Where to go from here
Once you have one working affiliate and one tracked conversion, the rest is recruiting. Our affiliate vs influencer post has channel-specific recruiting advice that works for SaaS audiences just as well as ecommerce. Our commission rate benchmarks post includes SaaS-specific ranges (20–50% — most other categories don't go that high).
If you want to skip the comparison shopping and try AffRef: 7-day free trial. Or read the full API docs if you want to see every field before signing up.