> Source: https://kaidn.io/docs/quickstart/nuxt
> 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. Configure the keys](#step-3)
- [4. Collect on submit](#step-4)
- [5. Score it in a server route](#step-5)
- [6. Test it](#step-6)
- [What the package does for you](#ip)
- [Next steps](#next)

# Nuxt quickstart

Nuxt gives you the browser and the server in one project, so a fraud integration is a composable on one side and a server route on the other. This is Nuxt 4, end to end.

Estimated time: under 15 minutes

### Before you start

- **Node 22+.** Nuxt 4 requires it.
- **Nuxt 4.** Client code lives in `app/` and server code in `server/`; on Nuxt 3 drop the `app/` prefix.
- **A free Kaidn account.** [Register](https://kaidn.io/register): 10,000 events a month, no card.

**You finish this one protected.** By the last step a real signup is scored and a `block` verdict actually stops it.

## 1. Get your two keys

- 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.

**Nuxt is private by default, and that is the opposite of Next.** In Next any variable named `NEXT_PUBLIC_*` is inlined into the browser bundle, so the dangerous thing is one prefix away. In Nuxt, everything in `runtimeConfig` stays on the server unless you nest it under `public`, so exposure is something you opt into on purpose. If you came from the Next guide, unlearn the prefix habit.

## 2. Set up your project

Terminal

```
npm create nuxt@latest kaidn-demo
cd kaidn-demo
npm install @kaidn/nuxt
```

One package covers both halves. `@kaidn/nuxt` gives you the composable for the browser, and `@kaidn/nuxt/server` gives you the scorer. Nothing imports both.

## 3. Configure the keys

Declare both in `runtimeConfig`. The empty strings are deliberate: they are placeholders that the environment fills at runtime, and they document which keys exist.

nuxt.config.ts

```
export default defineNuxtConfig({
  runtimeConfig: {
    // Server only. Never move this line inside `public`.
    kaidnApiKey: "",

    public: {
      // Sent to the browser on purpose.
      kaidnPk: "",
    },
  },
});
```

.env

```
NUXT_KAIDN_API_KEY=sk_live_your_secret_key
NUXT_PUBLIC_KAIDN_PK=pk_live_your_publishable_key
```

**The env names are not free-form.** Nuxt maps `NUXT_KAIDN_API_KEY` to `runtimeConfig.kaidnApiKey` and `NUXT_PUBLIC_KAIDN_PK` to `runtimeConfig.public.kaidnPk`, by uppercasing and splitting on the nesting. Reading `process.env.SOMETHING_ELSE` as a default in the config works at build time and then breaks at runtime, which is a bad way to find out.

## 4. Collect on submit

Wrap the package’s hook in your own composable so the key is read in one place. It collects 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/composables/useKaidn.ts

```
import { useDeviceId } from "@kaidn/nuxt";

export function useKaidn() {
  return useDeviceId(useRuntimeConfig().public.kaidnPk);
}
```

**The key is passed in, not discovered.** The package never imports `useRuntimeConfig`itself. Reaching into Nuxt’s virtual imports would save you this one line and tie the package to an internal that moves between versions, so it takes an argument instead and works the same on Nuxt 3, Nuxt 4 and plain Vue.

app/pages/signup.vue

```
<script setup lang="ts">
const { getDeviceId } = useKaidn();
const email = ref("");
const message = ref("");

async function onSubmit() {
  const deviceId = await getDeviceId();
  const res = await $fetch("/api/signup", {
    method: "POST",
    body: { email: email.value, deviceId },
  });
  message.value = res.message;
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <input v-model="email" type="email" required />
    <button type="submit">Create account</button>
    <p>{{ message }}</p>
  </form>
</template>
```

## 5. Score it in a server route

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

server/api/signup.post.ts

```
import { createKaidn } from "@kaidn/nuxt/server";

export default defineEventHandler(async (event) => {
  const { kaidnApiKey } = useRuntimeConfig(event);
  const kaidn = createKaidn({ apiKey: kaidnApiKey });

  const { email, deviceId } = await readBody(event);

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

  if (result.verdict === "block") {
    throw createError({ statusCode: 403, statusMessage: "Signup refused" });
  }
  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." };
});
```

**A server route is a public endpoint.** Anyone can POST `/api/signup` directly, without ever loading your form. So the verdict has to be enforced here, inside the handler, as above. Hiding the form or branching in the component is not a check.

`useRuntimeConfig(event)` takes the event on the server. Passing it is what lets Nuxt resolve per-request config correctly, so do not reach for `process.env` here.

## 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`.

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 handler, and both matter. They come out of the h3 event rather than out of your code.

#### The IP

`score()` calls h3’s `getRequestIP(event)`, which returns an address the server already resolved. 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.

**There is deliberately no option to trust `x-forwarded-for`.** h3 can be told to read it, and its own docs say to make sure the header can be trusted first, because the first entry is client-supplied: unless something you control overwrites it on every request, a caller can put any address they like there and walk through IP allow-lists, rate limits and geo checks. A fraud library offering that as a switch is offering a footgun, so it does not. If you genuinely need it, pass `ip` on the event yourself and it wins.

If you read our [Next.js guide](https://kaidn.io/docs/quickstart/nextjs) you saw the IP read out of `x-forwarded-for` by hand. That is not an inconsistency: a Next Server Action has no request object at all, so the header is the only thing available there. Here the server resolved it for you.

#### The device token

A raw fingerprint collides: two different people on the same phone model and browser can hash to the same id. The package reads the 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).

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.
