Back to guides
Next.jsIntermediate35 minutes

Next.js affiliate tracking with RefCampaign

Add RefCampaign to a Next.js app with browser click capture, email fallback attribution, and Stripe Checkout metadata.

4 min read

This guide adds RefCampaign to a Next.js app using the npm SDK. Your CMP explicitly enables or refuses browser attribution, and your checkout route passes a consented RefCampaign session into Stripe metadata.

Use this path when your app already has a Next.js frontend and a server route that creates Stripe Checkout sessions.

Prerequisites

  • A RefCampaign merchant account with one active campaign.
  • A RefCampaign public key and secret key from your dashboard.
  • A Next.js App Router project.
  • Stripe Checkout already installed or ready to install.

Install the SDK:

pnpm add @refcampaign/sdk stripe

Add the secret key to your server environment:

REFCAMPAIGN_SECRET_KEY=sk_live_your_refcampaign_secret
STRIPE_SECRET_KEY=sk_live_your_stripe_secret
NEXT_PUBLIC_APP_URL=https://yourapp.com

Connect browser attribution to your CMP

Create a client component near the root of your app. Pass it the persisted attribution choice resolved by your CMP: true, false, or null while the CMP is still loading.

// app/refcampaign-client.tsx
'use client'

import { useEffect } from 'react'
import { RefCampaignBrowser } from '@refcampaign/sdk'

export function RefCampaignClient({
  attributionConsent,
}: {
  attributionConsent: boolean | null
}) {
  useEffect(() => {
    RefCampaignBrowser.configure({
      siteToken: process.env.NEXT_PUBLIC_REFCAMPAIGN_SITE_TOKEN!,
    })
  }, [])

  useEffect(() => {
    if (attributionConsent === null) return
    void RefCampaignBrowser.setConsent({
      attribution: attributionConsent,
    })
  }, [attributionConsent])

  return null
}

Mount it in your root layout so every landing page receives the CMP state. Replay the stored choice after every reload and update the prop immediately after a grant or withdrawal.

// app/layout.tsx
import { RefCampaignClient } from './refcampaign-client'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <RefCampaignClient attributionConsent={cmp.attributionConsent} />
        {children}
      </body>
    </html>
  )
}

Identify users after signup or login

Call identify() only after consent, as soon as your app knows the user's email. RefCampaign hashes the email in the browser and attaches it to the current consented click.

// app/auth/identify-refcampaign.tsx
'use client'

import { useEffect } from 'react'
import { RefCampaignBrowser } from '@refcampaign/sdk'

type IdentifyRefCampaignProps = {
  email: string | null
}

export function IdentifyRefCampaign({ email }: IdentifyRefCampaignProps) {
  useEffect(() => {
    if (!email) return
    void RefCampaignBrowser.identify(email)
  }, [email])

  return null
}

Render this component after login or signup, where your session provider exposes the user email.

<IdentifyRefCampaign email={user.email} />

Create Stripe Checkout with RefCampaign metadata

Your checkout route should read the RefCampaign session cookie and pass it to RefCampaignServer.getStripeMetadata().

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { RefCampaignServer } from '@refcampaign/sdk'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const refcampaign = new RefCampaignServer(process.env.REFCAMPAIGN_SECRET_KEY!)

export async function POST(request: NextRequest) {
  const { priceId } = await request.json()
  const sessionId = request.cookies.get('_rc_sid')?.value

  const checkout = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    subscription_data: {
      metadata: refcampaign.getStripeMetadata(sessionId),
    },
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
  })

  return NextResponse.json({ url: checkout.url })
}

For one-time payments, put the metadata on payment_intent_data instead:

const checkout = await stripe.checkout.sessions.create({
  mode: 'payment',
  line_items: [{ price: priceId, quantity: 1 }],
  payment_intent_data: {
    metadata: refcampaign.getStripeMetadata(sessionId),
  },
  success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success`,
  cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
})

Test locally

Run your app and open https://yourapp.com/?ref=fabrice.

  • Refuse attribution: no RefCampaign capture request and no _rc_sid cookie.
  • Grant attribution: one capture request and a first-party _rc_sid cookie lasting 90 days.
  • Reload: the CMP must replay the persisted grant.
  • Withdraw consent: the cookie must be deleted.

Then create a checkout session and inspect the Stripe object:

  • subscriptions should contain metadata.refcampaign_session;
  • one-time payments should contain the same key on the Payment Intent;
  • no checkout should fail if _rc_sid is absent.

Common mistakes

Do not wait until the pricing page to forward the CMP decision. Affiliates often link to blog posts, comparison pages, or docs pages first.

Do not block checkout when no RefCampaign session exists. Direct customers should still buy normally.

Do not put subscription metadata on the Stripe Customer object. A customer can subscribe more than once through different affiliate journeys, so subscription-level metadata keeps attribution tied to the right purchase.

If consent is refused, do not recreate a browser session. Coupons, explicit referral codes, and independently known server-side affiliate codes remain valid alternatives when your order already contains them.

Next steps

Read the Stripe affiliate tracking guide for webhook-specific details, or use the SDK reference when you need every method signature. If you are still designing the program, the SaaS affiliate setup guide covers commission rules and operational defaults.