---
id: "api-reference/integrations/nextjs"
type: "doc"
url: "https://affitor.com/docs/api-reference/integrations/nextjs"
---
# 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**.

:::note

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`.

```bash
npm i @affitor/sdk
```

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

```tsx
// 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:

```tsx
// 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):

```ts
// 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.

:::note[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](/docs/api-reference/track-lead/#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?**

### Option A — Stripe integration (recommended for Stripe Checkout)

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.

```ts
// 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.

```ts
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

<VerifySuccess
  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"]}
  ifNotWorking={["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

<CommonMistakes items={[
  "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)."
]} />

<NextStep title="Using Clerk for auth?" description="Fire the signup from Clerk's user.created webhook." href="/docs/api-reference/integrations/nextjs-clerk" />
