Next.js + Clerk

Add Affitor tracking to a Next.js (App Router) app that uses Clerk for authentication.

This guide covers the auth-specific parts of an Affitor integration when your Next.js app uses Clerk. Steps 1 (capture the click) and 3 (track the sale) are identical to the base Next.js guide — this guide focuses on Step 2: tracking the signup with Clerk's identity model.

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

1. Capture the click

Follow Step 1 of the base Next.js guide — install @affitor/sdk, create the <AffitorInit /> client component, and render it in your root layout. Nothing changes for Clerk.

2. Track the signup

The affitor_click_id is stored in a first-party browser cookie. A server-side Clerk webhook (user.created) runs with no access to that cookie, so it cannot supply the click ID on its own.

Two paths are available — the client-side path is the reliable default.

After Clerk completes sign-up, call signup() from a client component. The browser SDK reads the affitor_click_id cookie automatically — you do not need to pass it explicitly.

Use Clerk's useUser() hook to access the signed-in user, then fire signup() once the user is available:

// app/post-signup.tsx  — render this on your post-signup or onboarding page
'use client';

import { useEffect } from 'react';
import { useUser } from '@clerk/nextjs';
import { signup } from '@affitor/sdk';

export function AffitorSignup() {
  const { user, isLoaded } = useUser();

  useEffect(() => {
    if (!isLoaded || !user) return;
    signup(user.id, user.primaryEmailAddress?.emailAddress);
  }, [isLoaded, user]);

  return null;
}

user.id is Clerk's stable userId — use this exact value as customerExternalId at sale time too.

:::note Place <AffitorSignup /> on the page a new user lands on right after registration (e.g. /onboarding, /welcome, or wherever your post-signup redirect goes). Rendering it on every page is harmless — signup() is idempotent — but placing it on the post-signup destination keeps attribution tight. :::

Option B — Clerk webhook (user.created)

If you need to track signups server-side via Clerk's user.created webhook, you must forward the click ID yourself. The webhook fires with no browser context, so Affitor cannot attribute the lead without it.

Step 1. On the client, read the click ID and store it on the Clerk user before sign-up completes (e.g. in a pre-submit handler or just before redirecting away from your sign-up form):

'use client';

import { useSignUp } from '@clerk/nextjs';
import { getClickId } from '@affitor/sdk';

// during your sign-up form submit
const { signUp } = useSignUp();

await signUp.update({
  unsafeMetadata: {
    affitor_click_id: getClickId() ?? null,
  },
});

Step 2. In your Clerk webhook handler, read the forwarded click ID and call trackLead:

// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import Affitor from '@affitor/sdk/server';

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

export async function POST(req: Request) {
  const payload = await req.text();
  const headers = {
    'svix-id': req.headers.get('svix-id')!,
    'svix-timestamp': req.headers.get('svix-timestamp')!,
    'svix-signature': req.headers.get('svix-signature')!,
  };

  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  const event = wh.verify(payload, headers) as { type: string; data: Record<string, unknown> };

  if (event.type === 'user.created') {
    const user = event.data;
    const unsafeMeta = (user.unsafe_metadata as Record<string, string> | undefined) ?? {};

    await affitor.trackLead({
      customerExternalId: user.id as string,
      clickId: unsafeMeta.affitor_click_id ?? undefined, // forwarded from the browser
      email: (user.email_addresses as Array<{ email_address: string }>)[0]?.email_address,
    });
  }

  return new Response('ok');
}

:::note If affitor_click_id is null (the user arrived directly, not via an affiliate link), trackLead still records the lead — it just won't be attributed to a partner. That is the correct behavior. :::

3. Track the sale

Follow Step 3 of the base Next.js guide. Use user.id (the Clerk user ID) as affitor_customer_key in Stripe metadata and as customerExternalId in trackSale — the same value you used at signup.

Verify

Verify success
Browser
  • Visit your site with `?aff=TESTCODE` — an `affitor_click_id` cookie is set
Network
  • `signup()` fires `POST /api/v1/track/lead` with a 2xx response after Clerk sign-up
Dashboard
  • The click and lead appear under your program's tracking events, attributed to the partner
If it doesn't work
  • Send `additional_data: { test_mode: true }` to create test events without commissions
  • If using the webhook path and leads are unattributed, confirm `affitor_click_id` is present in `unsafe_metadata` before the webhook fires

Common mistakes

Common mistakes
  • Using Option B (webhook) without forwarding the click ID — the `user.created` webhook has no browser cookie access, so leads arrive with no partner attribution.
  • Different customer IDs at signup vs sale — `user.id` from Clerk must be used as `affitor_customer_key` and `customerExternalId` consistently.
  • Reading `getClickId()` too late — call it before Clerk redirects away from the sign-up page, otherwise the value may not be available in the new page context.
  • Calling `signup()` in a server component or route handler — it must run in a client component where the cookie is accessible.
  • Subscriptions missing `subscription_data.metadata` — renewals won't attribute to any partner.
Next recommended step
Add Stripe for sales tracking

Attach Affitor metadata to Stripe Checkout Sessions so sales and renewals attribute automatically.

Continue
Edit on GitHub
© 2026 Affitor