> Source: https://kaidn.io/docs/quickstart/nextjs
> Full documentation index: https://kaidn.io/llms.txt

Docs menuAll pages, quickstarts and this page’s contentsDocs
- [Introduction](https://kaidn.io/docs)
- [Quickstarts](https://kaidn.io/docs/quickstart)
- [Core concepts](https://kaidn.io/docs/concepts)
- [API reference](https://kaidn.io/docs/api)
- [Guides](https://kaidn.io/docs/guides)
- [Keys & dashboard](https://kaidn.io/docs/keys)
- [Glossary](https://kaidn.io/glossary)

Web
- [JavaScript](https://kaidn.io/docs/quickstart/javascript)
- [React](https://kaidn.io/docs/quickstart/react)
- [Next.js](https://kaidn.io/docs/quickstart/nextjs)
- [Preact](https://kaidn.io/docs/quickstart/preact)
- [Vue](https://kaidn.io/docs/quickstart/vue)
- [Nuxt](https://kaidn.io/docs/quickstart/nuxt)
- [Angular](https://kaidn.io/docs/quickstart/angular)
- [Svelte](https://kaidn.io/docs/quickstart/svelte)

Mobile
- Androidsoon
- iOSsoon
- React Nativesoon
- Fluttersoon

Server
- [Node.js](https://kaidn.io/docs#quickstart)
- [PHP](https://kaidn.io/docs/quickstart/php)
- C#/.NETsoon
- Gosoon
- Javasoon
- [Python](https://kaidn.io/docs/quickstart/python)

On this page
- [1. Get your two keys](#step-1)
- [2. Set up your project](#step-2)
- [3. Add the provider](#step-3)
- [4. Build the signup form](#step-4)
- [5. Score it in a Server Action](#step-5)
- [6. Test it](#step-6)
- [What the package does for you](#free)
- [A Route Handler instead](#route-handler)
- [Next steps](#next)

# Next.js quickstart

Next.js is the one stack where both halves of a fraud integration live in the same project: the browser collects a device id, and a Server Action a few files away scores it with your secret key. This is the App Router, end to end.

Estimated time: under 10 minutes

### Before you start

- **Node 20+ and npm.**
- **Next.js 15 or later, App Router.** The package uses `await headers()` and `await cookies()`, which became async in 15.
- **A free Kaidn account.** [Register](https://kaidn.io/register): 10,000 events a month, no card.

**Unlike the browser-only guides, you finish this one protected.** By the last step a real signup is scored and a `block` verdict actually stops it. If you only want the frontend half, the [React quickstart](https://kaidn.io/docs/quickstart/react) stops at the device id.

## 1. Get your two keys

Next.js needs both, and the difference matters more here than anywhere else because both live in the same `.env.local`.

- A **publishable key** (`pk_live_…`) is read by the browser. It is meant to be public.
- A **secret key** (`sk_live_…`) is what scores events. It must never reach the browser.

Both are in your [dashboard](https://kaidn.io/app/keys). The secret key is shown once.

.env.local

```
# Read in the browser. Public by design.
NEXT_PUBLIC_KAIDN_PK=pk_live_your_publishable_key

# Server only. Never prefix this one.
KAIDN_API_KEY=sk_live_your_secret_key
```

**Never write `NEXT_PUBLIC_KAIDN_API_KEY`.** That prefix is not a naming convention, it is an instruction: Next inlines any `NEXT_PUBLIC_` variable into the JavaScript it sends to the browser. A secret key with that prefix is published to every visitor, and it will not throw, warn, or break anything. If it ever happens, rotate the key; the deploy that removes it does not un-publish it.

## 2. Set up your project

Terminal

```
npx create-next-app@latest kaidn-demo --app --ts
cd kaidn-demo
npm install @kaidn/nextjs
```

One package covers both halves. `@kaidn/nextjs` gives you the provider and hooks for the browser, and `@kaidn/nextjs/server` gives you the scorer. They are separate entry points for a reason worth knowing, in step 5.

## 3. Add the provider

Wrap the app once in the root layout. The provider holds configuration and **collects nothing on its own**, so it is safe on pages with no signup on them. It is already marked `"use client"` inside the package, so it drops into a Server Component layout as-is.

app/layout.tsx

```
import { KaidnProvider } from "@kaidn/nextjs";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <KaidnProvider publishableKey={process.env.NEXT_PUBLIC_KAIDN_PK!}>
          {children}
        </KaidnProvider>
      </body>
    </html>
  );
}
```

There is no SSR guard to write. `deviceId` is `null` through server rendering and the first hydration pass, so both renders produce the same output and there is no mismatch to work around.

## 4. Build the signup form

The form is a Client Component, because collecting a device id is a browser job. `getDeviceId()` runs when you ask it to rather than on mount, which puts the work at the moment somebody acts and keeps fingerprinting off pages that do not need it.

app/signup-form.tsx

```
"use client";

import { useState } from "react";
import { useDeviceId } from "@kaidn/nextjs";
import { signup } from "./actions";

export function SignupForm() {
  const { getDeviceId } = useDeviceId();
  const [message, setMessage] = useState("");

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const form = new FormData(e.currentTarget);

    // Never throws. A null id is a signal you are missing, not a broken form.
    const deviceId = await getDeviceId();

    const result = await signup({
      email: String(form.get("email")),
      deviceId,
    });
    setMessage(result.message);
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <button type="submit">Create account</button>
      <p>{message}</p>
    </form>
  );
}
```

## 5. Score it in a Server Action

This is the half that actually stops anything. Note the import path: `@kaidn/nextjs/server`, not `@kaidn/nextjs`.

app/actions.ts

```
"use server";

import { createKaidn } from "@kaidn/nextjs/server";

const kaidn = createKaidn({ apiKey: process.env.KAIDN_API_KEY! });

export async function signup({ email, deviceId }: { email: string; deviceId: string | null }) {
  const result = await kaidn.score({
    event: "signup",
    email,
    device_id: deviceId ?? undefined,
  });

  if (result.verdict === "block") {
    return { ok: false, message: "We could not create that account." };
  }
  if (result.verdict === "review") {
    // Let them in, flag it for a human. Most fraud lives here, not in "block".
    await flagForReview(email, result.reasons);
  }

  await createUser(email);
  return { ok: true, message: "Account created." };
}
```

**That import path is a safety device, not a naming choice.** `@kaidn/nextjs/server` begins with `import "server-only"`, so importing it from a Client Component is a **build error**rather than a secret key compiled into your browser bundle. It is the half of step 1’s warning that a machine can enforce instead of a person remembering.

**A Server Action is a public POST endpoint.** Next compiles it to an action id that anyone can call directly, which is why its own documentation says to treat every action as an untrusted entry point. For you that has one concrete consequence: the verdict has to be enforced *inside* the action, as above. Deciding in the component and rendering a different form is not a check, because the request never had to come from your UI.

## 6. Test it

Terminal

```
npm run dev
```

Submit the form, then open your [events](https://kaidn.io/app/events). A scored signup appears with its verdict and the reasons behind it. Submit twice from the same browser and the second one carries `device_reuse`, which is the check most people are here for.

Output

```
{
  "score": 15,
  "verdict": "allow",
  "reasons": ["device_reuse"],
  "reason_text": "This device is linked to 2 accounts."
}
```

`reasons` holds machine-readable codes to branch on; `reason_text` is the sentence written for a human. Log the second one, switch on the first.

## What the package does for you

Two fields never appeared in that action, and both matter. They come out of the request rather than out of your code.

#### The IP

A Server Action has no request object. Written by hand, this is the field people forget, and forgetting it is quiet: the event goes out with no `ip`, every IP signal (datacenter, proxy, ASN) stops firing, and the integration still looks like it works. `score()` reads it from `x-forwarded-for` for you.

**`x-forwarded-for` is only as trustworthy as whatever sets it.** On Vercel it is set for you. Behind your own nginx or a CDN, make sure the header is overwritten at the edge rather than passed through, or a caller can put any IP they like in it. On Cloudflare, pass `ipHeaders: ["cf-connecting-ip"]` to `createKaidn`.

#### The device token

A raw fingerprint collides: two different people on the same iPhone model and browser can hash to the same id. The package reads a first-party cookie on the way in and writes the new token back on the way out, which turns a guess into a browser you actually recognise. What that buys you is described in [device identity](https://kaidn.io/docs/concepts#identity).

Calling `score()` from a Server Component works, but the cookie is not written: HTTP cannot set one once streaming has started, and Next forbids it. The token still comes back on `result.device_token` if you want to persist it from an action instead.

## A Route Handler instead

If your form posts with `fetch` rather than calling an action, the same client works unchanged. Route Handlers can write cookies too, so nothing is lost.

app/api/signup/route.ts

```
import { NextResponse } from "next/server";
import { createKaidn } from "@kaidn/nextjs/server";

const kaidn = createKaidn({ apiKey: process.env.KAIDN_API_KEY! });

export async function POST(request: Request) {
  const { email, deviceId } = await request.json();

  const result = await kaidn.score({
    event: "signup",
    email,
    device_id: deviceId ?? undefined,
  });

  if (result.verdict === "block") {
    return NextResponse.json({ ok: false }, { status: 403 });
  }
  return NextResponse.json({ ok: true });
}
```

Need something the wrapper does not cover, like `/v1/check/email` or a label? `kaidn.client` is the underlying `@kaidn/sdk` instance, with every endpoint on it.

## Next steps

- [Core concepts](https://kaidn.io/docs/concepts) — how a verdict is made, and where the engine fails. Six ideas.
- [Guides](https://kaidn.io/docs/guides) — guarding a cashout, catching account takeover, stopping multi-accounting.
- [API reference](https://kaidn.io/docs/api) — every field on the event and every reason code that can come back.
