---
id: "api-reference/integrations/nextjs-nextauth"
type: "doc"
url: "https://affitor.com/docs/api-reference/integrations/nextjs-nextauth"
---
# Next.js + NextAuth

> Add Affitor lead tracking to a Next.js app using NextAuth (Auth.js) — handle the click-id cookie gap correctly.

This guide covers the signup step when your Next.js app uses **NextAuth (Auth.js)** for authentication. Steps 1 (capture click) and 3 (track sale) are identical to the [base Next.js guide](/docs/api-reference/integrations/nextjs) — this page focuses on what is different: wiring the lead call to NextAuth's auth lifecycle without losing the click attribution.

:::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
- A Next.js App Router app with NextAuth (Auth.js) configured

## 1. Capture the click

Follow [Step 1 of the Next.js guide](/docs/api-reference/integrations/nextjs#1-capture-the-click). Install `@affitor/sdk`, create the client component that wraps your app in `<AffitorProvider>`, and render it in your root layout. No changes needed for NextAuth.

## 2. Track the signup

### The cookie gap

NextAuth's `events.createUser` callback fires **on the server**, once per new user — it looks like the ideal place to call `trackLead`. It is — but with one important caveat:

**The `affitor_click_id` is a first-party browser cookie.** A server-side callback has no access to it. If you call `@affitor/sdk/server trackLead` from `events.createUser` without a `clickId`, Affitor cannot find the click: the call returns `400` and no lead is recorded.

### Recommended: a server route called after sign-in

Send the lead from your own route handler, with your program API key. A request from the browser to your route handler carries the first-party `affitor_click_id` cookie, so the server can read it directly.

Stock Auth.js sessions do not include `user.id`. Expose it once in your auth config, and add the type so `session.user.id` compiles:

```ts
// auth.ts
export const { auth, handlers, signIn, signOut } = NextAuth({
  // ...your providers and adapter
  callbacks: {
    session({ session, user, token }) {
      session.user.id = user?.id ?? token.sub ?? '';
      return session;
    },
  },
});

// types/next-auth.d.ts
import type { DefaultSession } from 'next-auth';
declare module 'next-auth' {
  interface Session {
    user: { id: string } & DefaultSession['user'];
  }
}
```

Then the route:

```ts
// app/api/affitor/lead/route.ts
import { cookies } from 'next/headers';
import Affitor from '@affitor/sdk/server';
import { auth } from '@/auth'; // your NextAuth (Auth.js) instance

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

// Needs the session callback above; without it session.user.id is empty and no lead is sent.
export async function POST() {
  const session = await auth();
  const clickId = (await cookies()).get('affitor_click_id')?.value;
  if (!session?.user?.id || !clickId) return new Response(null, { status: 204 });

  // user.id is the NextAuth user ID — use it as customerExternalId everywhere:
  // signup, sale, Stripe metadata.
  await affitor.trackLead({
    customerExternalId: session.user.id,
    clickId,
    email: session.user.email ?? undefined,
  });
  return new Response(null, { status: 204 });
}
```

Call it once from the browser after NextAuth confirms a new user session:

```tsx
// app/auth-tracker.tsx
'use client';

import { useEffect, useRef } from 'react';
import { useSession } from 'next-auth/react';

export function AffitorAuthTracker() {
  const { status } = useSession();
  const tracked = useRef(false);

  useEffect(() => {
    if (status !== 'authenticated' || tracked.current) return;
    tracked.current = true;
    fetch('/api/affitor/lead', { method: 'POST' });
  }, [status]);

  return null;
}
```

Render it inside your root layout, inside `SessionProvider`:

```tsx
// app/layout.tsx
import { Providers } from './affitor-provider';
import { AffitorAuthTracker } from './auth-tracker';
import { SessionProvider } from 'next-auth/react';

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

:::note

A lead is recorded once per customer. A repeat call for the same customer (for example on a later sign-in) returns `400` from `trackLead`, which resolves with `ok: false` instead of throwing, so it is safe to ignore.

:::

:::note[Browser `signup()` is provisional on new programs]

The tracker's browser helper, `affitor.signup(id, email)` from `useAffitor()`, sends the lead without your API key. New programs require server-verified leads, so a browser-only lead is stored as **provisional** and does not settle, save the user ID, or count until your server sends the lead 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).

:::

### Alternative: server-side via events.createUser with click-id forwarding

If you need the lead recorded the moment the user row is created (before the client redirects), you can use `events.createUser` — but you **must** forward the click ID from the browser yourself.

**Step A — read the click ID on the client before the user submits the sign-up form:**

```tsx
'use client';
import { useAffitor } from '@affitor/sdk/react';

// In your sign-up form component, read the click ID and include
// it in the request or store it on the user's session/metadata.
const affitor = useAffitor(); // null until the tracker has loaded
const clickId = affitor?.getClickId() ?? null; // the affitor_click_id cookie value
```

**Step B — pass it through to your NextAuth config and call `trackLead` in the event:**

```ts
// auth.ts (NextAuth config)
import Affitor from '@affitor/sdk/server';
import type { NextAuthConfig } from 'next-auth';

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

export const authConfig: NextAuthConfig = {
  // ...providers, callbacks, etc.
  events: {
    async createUser({ user }) {
      // user.id is the NextAuth-generated user ID — use it as
      // customerExternalId everywhere: signup, sale, Stripe metadata.
      await affitor.trackLead({
        customerExternalId: user.id,
        clickId: user.affitorClickId ?? undefined, // forwarded from the browser
        email: user.email ?? undefined,
      });
    },
  },
};
```

:::note

`user.affitorClickId` is illustrative — the forwarding mechanism depends on your auth flow. Common approaches: store the click ID in the sign-up form's hidden field and write it to a custom user attribute before `createUser` fires, or pass it through a custom `session` object. Without it, `clickId` is undefined and the lead call returns `400` with nothing recorded.

:::

If you cannot reliably forward the click ID into `events.createUser`, **use the server route above.**

## 3. Track the sale

Follow [Step 3 of the Next.js guide](/docs/api-reference/integrations/nextjs#3-track-the-sale). Use `session.user.id` as `affitor_customer_key` (Stripe path) or `customerExternalId` (server SDK path) — it must match the value you sent at signup.

## Verify

<VerifySuccess
  browser={["Visit your site with `?aff=TESTCODE` — an `affitor_click_id` cookie is set"]}
  network={["After sign-in, your `/api/affitor/lead` route's `trackLead()` call returns 2xx"]}
  dashboard={["The lead appears under your program's tracking events, attributed to the correct partner"]}
  ifNotWorking={["If no lead appears, the click ID is not reaching your server — confirm the `affitor_click_id` cookie is set before the user signs in. Create a test lead from your server with `affitor.trackLead({ customerExternalId: 'test_customer', additionalData: { test_mode: true, program_id: YOUR_PROGRAM_ID } })`."]}
/>

## Common mistakes

<CommonMistakes items={[
  "Calling trackLead from events.createUser without a clickId — the call returns 400 and no lead is recorded. Use the server route above or forward the click ID explicitly.",
  "Different customer IDs at signup vs sale — `session.user.id` must be used consistently as `customerExternalId` / `affitor_customer_key` everywhere.",
  "Wrapping AffitorAuthTracker outside SessionProvider — useSession() will throw. SessionProvider must be an ancestor.",
  "Trying to track a sale from the browser — sales are server-side only (Stripe metadata or the server SDK)."
]} />

<NextStep title="Ready to track sales?" description="Connect Stripe so renewals and one-time payments attribute automatically." href="/docs/api-reference/integrations/stripe" />
