Back to all posts

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

  1. Affiliate shares a link: https://yourapp.com?ref=SARAH
  2. 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.
  3. Shopper signs up + pays. At Stripe Checkout creation you pull the cookie value and inject it into subscription_data.metadata.affiliate_code.
  4. 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.created or checkout.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:

  1. Open incognito Chrome / Safari (avoid existing cookies).
  2. Visit https://yourapp.com?ref=SARAH
  3. Open DevTools → Application → Cookies. Confirm the affref cookie is set to SARAH.
  4. Complete a real signup + payment (use a test card or your own real card; you can refund yourself after).
  5. 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.

Ready to grow your affiliate program?

Start your 7-day free trial. Cancel anytime.

View Plans
Usually replies within an hour
Hey 👋 I'm Aaron, founder of AffRef. Drop your email and your question and I'll get back to you fast.
By chatting you agree to our privacy policy.
End this chat? History on this device clears.