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.
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 Router app with Clerk configured
1. Capture the click
Follow Step 1 of the base Next.js guide — install @affitor/sdk, create the client component that wraps your app in <AffitorProvider>, 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 webhook path (server-side, with your API key) is the one that settles the lead: new programs require server-verified leads, and a lead sent only from the browser stays provisional. See Lead auth policy.
Option A — Clerk webhook (user.created, recommended)
Track the signup server-side from 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 { useAffitor } from '@affitor/sdk/react';
// inside your sign-up form component
const { signUp } = useSignUp();
const affitor = useAffitor(); // null until the tracker has loaded
// during your sign-up form submit
await signUp.update({
unsafeMetadata: {
affitor_click_id: affitor?.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');
}If affitor_click_id is null (the user arrived directly, not via an affiliate link), Affitor returns 400 and records nothing, because there is no click to link the lead to. That is expected for non-referred users; trackLead resolves with ok: false rather than throwing.
Option B — Client-side signup()
After Clerk completes sign-up, call the tracker's signup() from a client component. The tracker reads the affitor_click_id cookie automatically — you do not need to pass it explicitly. On programs that require server-verified leads (the default for new programs), this lead is provisional: it does not settle, does not save the user ID, and does not count or pay per lead until the webhook path above 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. Use it alongside Option A, not instead of it.
Use Clerk's useUser() hook to access the signed-in user and useAffitor() to access the tracker, then fire signup() once both are 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 { useAffitor } from '@affitor/sdk/react';
export function AffitorSignup() {
const { user, isLoaded } = useUser();
const affitor = useAffitor(); // null until the tracker has loaded
useEffect(() => {
if (!isLoaded || !user || !affitor) return;
const email = user.primaryEmailAddress?.emailAddress;
if (email) affitor.signup(user.id, email);
}, [isLoaded, user, affitor]);
return null;
}user.id is Clerk's stable userId — use this exact value as customerExternalId at sale time too.
Place <AffitorSignup /> only 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 adds nothing: on programs that require server-verified leads, a repeat browser lead returns 200 with provisional: true and records nothing new, and on older programs where a browser lead settles directly, a repeat after the lead has settled is rejected with 400.
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
- Visit your site with
?aff=TESTCODE— anaffitor_click_idcookie is set
- Your Clerk webhook's
trackLead()call toPOST /api/v1/track/leadreturns 2xx withoutprovisional: true
- The click and lead appear under your program's tracking events, attributed to the partner
- Create a test lead from your server:
affitor.trackLead({ customerExternalId: 'test_customer', additionalData: { test_mode: true, program_id: YOUR_PROGRAM_ID } }) - If using the webhook path and leads are unattributed, confirm
affitor_click_idis present inunsafe_metadatabefore the webhook fires
Common mistakes
- Using Option A (webhook) without forwarding the click ID — the
user.createdwebhook has no browser cookie access, so the lead call returns 400 and nothing is recorded. - Different customer IDs at signup vs sale —
user.idfrom Clerk must be used asaffitor_customer_keyandcustomerExternalIdconsistently. - 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.