Next.js

Add Affitor tracking to a Next.js (App Router) app — clicks, signups, and sales.

This guide wires Affitor into a Next.js App Router app with the three moves every integration needs: capture the click, track the signup, track the sale.

Info

The @affitor/sdk package is Beta. The documented happy-path works; report issues on GitHub.

Prerequisites

  • Your program ID (dashboard → program settings)
  • A program API key for server-side calls (leads and sales)
  • A Next.js app on the App Router

1. Capture the click

Install the SDK and load the tracker once on the client. The tracker reads ?aff= from the URL and stores a first-party affitor_click_id cookie. AffitorProvider (from @affitor/sdk/react) loads it in the browser; on the server it does nothing and useAffitor() returns null.

npm i @affitor/sdk

Create a client component that wraps your app in the provider:

// app/affitor-provider.tsx
'use client';

import { AffitorProvider } from '@affitor/sdk/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return <AffitorProvider programId="YOUR_PROGRAM_ID">{children}</AffitorProvider>;
}

Render it once in your root layout:

// app/layout.tsx
import { Providers } from './affitor-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

2. Track the signup

When a user finishes signing up, link them to their click from your server, with your own stable user ID — you'll reuse this exact value at sale time. Send it with the server SDK and your program API key (e.g. in a route handler or right after the DB write):

// server only — e.g. app/api/signup/route.ts
import { cookies } from 'next/headers';
import Affitor from '@affitor/sdk/server';

const affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });

// after the user row is created
const clickId = (await cookies()).get('affitor_click_id')?.value;
if (clickId) {
  await affitor.trackLead({
    customerExternalId: user.id, // same ID you'll send at sale time
    clickId,                     // affitor_click_id, forwarded from the cookie
    email: user.email,           // optional
  });
}

A lead needs the click: without a clickId (or a customerExternalId Affitor already knows), the API returns 400 and records nothing, which is expected for visitors who did not come through a partner link.

Browser signup() is provisional on new programs

The tracker also has a browser helper, affitor.signup(user.id, user.email) (from useAffitor() in @affitor/sdk/react). New programs require server-verified leads, so a browser-only signup is stored as provisional: the API returns 200 with provisional: true, but the lead does not settle, does not save your user ID, and does not count or pay per lead until your server sends the lead above or a Stripe Checkout payment for that customer arrives through your connected Stripe account. A sale sent to /api/v1/track/sale does not settle it. See Lead auth policy.

3. Track the sale

Pick one of two paths — this is the real choice: does Stripe tell us, or does your server?

Attach Affitor metadata when you create the Checkout Session in your own Stripe account. Affitor reads your Stripe webhooks and attributes the sale automatically — no extra call.

// server — creating the Checkout Session
const session = await stripe.checkout.sessions.create({
  mode: 'subscription', // or 'payment'
  line_items: [{ price: 'price_xxx', quantity: 1 }],
  success_url: 'https://yoursite.com/success',
  cancel_url: 'https://yoursite.com/cancel',
  metadata: {
    affitor_click_id: clickId,        // from the affitor_click_id cookie
    affitor_customer_key: user.id,    // SAME id you used at signup
    program_id: 'YOUR_PROGRAM_ID',
  },
  // subscriptions: duplicate the SAME metadata so renewals attribute
  subscription_data: {
    metadata: {
      affitor_click_id: clickId,
      affitor_customer_key: user.id,
      program_id: 'YOUR_PROGRAM_ID',
    },
  },
});

Option B — Server-side tracking (any provider)

Report the finalized sale from your backend. Works with any payment provider.

import Affitor from '@affitor/sdk/server';
const affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });

await affitor.trackSale({
  customerExternalId: user.id, // SAME id as signup
  amount: 4999,                // integer cents
  invoiceId: invoice.id,       // idempotency key — a duplicate returns 409
});

Verify

Verify success
Browser
  • Visit your site with ?aff=TESTCODE — an affitor_click_id cookie is set
Network
  • Your server's trackLead() call to POST /api/v1/track/lead returns 2xx without provisional: true
Dashboard
  • The click, lead, and sale appear under your program's tracking events
If it doesn't work
  • Create a test lead from your server: affitor.trackLead({ customerExternalId: 'test_customer', additionalData: { test_mode: true, program_id: YOUR_PROGRAM_ID } }). For a test sale, call POST /api/v1/track/sale directly with your API key and additional_data: { test_mode: true } — the SDK's trackSale() cannot send it

Common mistakes

Common mistakes
  • Different customer IDs at signup vs sale — affitor_customer_key must equal the customer_key/customerExternalId you sent at signup.
  • Subscriptions missing subscription_data.metadata — renewals won't attribute.
  • Loading the tracker on the server — AffitorProvider and loadAffitor() must run in a client component (on the server they resolve to null).
  • Relying on the browser signup() alone — on new programs it stays provisional until your server sends the lead.
  • Trying to track a sale from the browser — sales are server-side only (Stripe metadata or the server SDK).
Next recommended step
Using Clerk for auth?

Fire the signup from Clerk's user.created webhook.

Continue →
© 2026 Affitor