Specify docs

Next.js

Track conversions in a Next.js app, in the browser and on the server.

Set up with the wizard

npx @specify-sh/wizard --advertiser

The wizard runs through the coding agent you already use (Claude Code, Codex etc.). It inspects your codebase, proposes which events to capture and implements them as a reviewable diff.

Prefer to do it by hand? The manual steps below are the same setup.

Manual setup

Runnable example

nextjs-typescript is this guide as a working App Router app.

1. Install the SDK

npm i @specify-sh/advertiser-sdk

2. Set your key

You will need an advertiser key. See Advertiser keys.

Use the NEXT_PUBLIC_ prefix so the browser can read it. Advertiser keys are public, and server code reads the same variable.

.env.local
NEXT_PUBLIC_SPECIFY_ADVERTISER_KEY=sak_your_key_here

3. Create the client

Create the client in its own module and import it from Client Components.

lib/specify.ts
import Specify from '@specify-sh/advertiser-sdk';

export const specify = new Specify({
  advertiserKey: process.env.NEXT_PUBLIC_SPECIFY_ADVERTISER_KEY!
});

Importing it from a Server Component fails the build with a message pointing to the server entry, used in step 6.

Add one Client Component to your root layout. It passes your consent manager's answer on every page load, and identifies the signed-in user and any connected wallet.

components/SpecifyBridge.tsx
'use client';

import { useEffect } from 'react';
import { useAccount } from 'wagmi';
import { useUser } from '@/auth';
import { specify } from '@/lib/specify';

export function SpecifyBridge() {
  const user = useUser();
  const { address } = useAccount();

  // Adapt this to your consent manager
  useEffect(() => {
    specify.setCookieConsent(cmp.getConsent().advertising);

    return cmp.onConsentChange((consent) => {
      specify.setCookieConsent(consent.advertising);
    });
  }, []);

  useEffect(() => {
    if (user) specify.identify(user.id);
  }, [user]);

  useEffect(() => {
    if (address) specify.identify(address);
  }, [address]);

  return null;
}

Use your own user ID, not an email address, and give each person their own. Call specify.reset() in your sign-out handler.

5. Capture conversions in the browser

Capture an event once the action has succeeded.

components/SignupForm.tsx
'use client';

import { specify } from '@/lib/specify';

async function onSubmit(form: SignupForm) {
  await createAccount(form);
  specify.capture('signup');
}

6. Capture conversions on the server

For conversions your backend confirms, use the server entry in a route handler or server action. Pass the same user ID the browser identified.

app/api/payments/webhook/route.ts
import { capture } from '@specify-sh/advertiser-sdk/server';

export async function POST(request: Request) {
  const payment = await verifyPayment(request);

  await capture({
    advertiserKey: process.env.NEXT_PUBLIC_SPECIFY_ADVERTISER_KEY!,
    event: 'purchase',
    identity: payment.userId,
    dedupeKey: payment.id
  });

  return new Response(null, { status: 204 });
}

See Server events for the options and errors.

7. Check it works

Use a development key and open your browser's network panel. After the consent call, requests to spfsrv.com/v1/identify and spfsrv.com/v1/events should return 200. Development keys never record events.

Next steps

On this page