Docs menuAll pages, quickstarts and this page’s contents

React quickstart

Add Kaidn to a React app and give every visitor a stable device id you can send to your backend. Scaffolded with Vite, and the example is the one most people start with: stopping the same person opening account after account.

Estimated time: under 10 minutes

Before you start

  • Node 20+ and npm, to run Vite.
  • React 18 or later. Next.js and Remix both work; see step 4.
  • A free Kaidn account. Register: 10,000 events a month, no card.

This is the frontend half, and on its own it blocks nothing. By the end you will have a device id. That only becomes fraud prevention when your server sends it to /v1/score and acts on the verdict, so finish with a backend quickstart: Python, PHP or Node.js.

01

Get your publishable key

  1. Create an account if you do not have one.
  2. Go to Fraud Scoring API → Device trackers, create a tracker, and list the domains it may run on. Include localhost while you build.
  3. Copy the publishable key. It starts pk_live_.

This key is meant to be visible in your bundle. It is domain-locked and can only send fingerprints: it cannot score, read your data, or work from a site you did not authorise. Your kdn_live_ secret key is the opposite, and passing one to the provider throws immediately rather than letting it ship to every visitor.

02

Set up your project

Skip to step 3 if you have a project already.

Terminal
npm create vite@latest kaidn-react-quickstart -- --template react
cd kaidn-react-quickstart
npm install

Run it and open http://localhost:5173, Vite's default.

Terminal
npm run dev
03

Build the signup form

A component to attach to. Create src/SignupForm.jsx:

src/SignupForm.jsx
export default function SignupForm({ onSubmit, isLoading }) {
  return (
    <form
      className="wrap"
      onSubmit={(e) => {
        e.preventDefault();
        onSubmit(new FormData(e.currentTarget));
      }}
    >
      <h1>Create an account</h1>

      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required
             placeholder="you@example.com" />

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" required />

      <button type="submit" disabled={isLoading}>
        {isLoading ? "Checking…" : "Create account"}
      </button>
    </form>
  );
}

And enough CSS to see it. Append to src/App.css:

src/App.css
.wrap {
  max-width: 380px; min-height: 100vh; margin: 0 auto;
  display: flex; flex-direction: column; justify-content: center; gap: .5rem;
  padding: 1rem; text-align: left;
}

input { padding: .6rem; border: 1px solid #ccc; border-radius: 6px; font: inherit; }
label { font-size: .85rem; color: #555; }

button {
  margin-top: .75rem; padding: .7rem 1.2rem; font: inherit; cursor: pointer;
  background: #111; color: #fff; border: 0; border-radius: 6px;
}
button:disabled { opacity: .6; cursor: not-allowed; }
04

Add the provider

Terminal
npm install @kaidn/react

Wrap your app once, at the root. The provider holds configuration and collects nothing on its own, so it is safe on every page including the ones with no signup on them.

src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { KaidnProvider } from "@kaidn/react";
import App from "./App.jsx";
import "./App.css";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <KaidnProvider publishableKey="pk_live_your_key_here">
      <App />
    </KaidnProvider>
  </StrictMode>
);

For production, use an environment variable. Vite exposes anything prefixed VITE_, so import.meta.env.VITE_KAIDN_PK keeps it out of your repository. It is not a secret, but a key you can rotate without a code change is worth having.

Next.js and Remix

Same provider, 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. In the App Router the provider is already marked "use client", so it drops into a layout as-is:

app/layout.jsx
import { KaidnProvider } from "@kaidn/react";

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

Collect on submit

useDeviceId() collects when you ask it to, not on mount. That puts the work at the moment somebody acts, which is when the signal is freshest, and keeps fingerprinting off pages that do not need it.

src/App.jsx
import { useDeviceId } from "@kaidn/react";
import SignupForm from "./SignupForm.jsx";

export default function App() {
  const { getDeviceId, isLoading } = useDeviceId();

  async function handleSubmit(form) {
    // Never throws. A blocked script or an ad blocker returns null, and your
    // signup carries on with one signal fewer.
    const deviceId = await getDeviceId();

    await fetch("/api/signup", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        email: form.get("email"),
        password: form.get("password"),
        deviceId,          // your server scores THIS
      }),
    });
  }

  return <SignupForm onSubmit={handleSubmit} isLoading={isLoading} />;
}

The hook gives you four things:

  • getDeviceId() collects and returns string | null. Never throws.
  • deviceId is the last id collected, or null before the first call.
  • isLoading is true while a collection is in flight. Disable your button with it.
  • error is whatever went wrong last, for your logging rather than your user.

Note what is not here: no verdict and no decision. The browser is an untrusted place to make one, so it never sees a score. It produces an id; your server produces the judgement.

A double-clicked button fingerprints once, not twice.The hook keeps one collection in flight at a time, which also means React's StrictMode double-invoke in development does not look like a double-billing bug in your code.

06

Score it on your server

This is the step that turns a device id into fraud prevention. Everything before it collects; nothing before it decides.

Your /api/signup handler takes the id and passes it to /v1/score. Node, since you are already in a JavaScript project:

C#/.NETsoonGosoonJavasoon

npm install @kaidn/sdk

api/signup.js
import { Kaidn } from "@kaidn/sdk";

// Your SECRET key. Server only, never the browser.
const kaidn = new Kaidn({ apiKey: process.env.KAIDN_API_KEY });

// Express, Fastify, Hono, a Next.js route handler: the call is the same.
app.post("/api/signup", async (req, res) => {
  let r;
  try {
    r = await kaidn.score({
      event: "signup",
      ip: req.ip,
      email: req.body.email,
      device_id: req.body.kaidn_device_id,   // the id the browser collected
    });
  } catch {
    // FAIL OPEN. An outage in your fraud vendor must never become an
    // outage in your signup form.
    return res.json({ ok: true });
  }

  if (r.verdict === "block") {
    // Do not name the signal. It teaches the next attempt.
    return res.status(403).json({ error: "We could not create that account." });
  }

  const user = await createUser(req.body);
  if (r.verdict === "review") await flagForReview(user.id, r.event_id, r.reason_text);

  res.json({ ok: true });
});

Three answers, and you decide what each one means. Kaidn never blocks anybody on your behalf.

Whichever you pick, the client covers every endpoint an API key can reach, not just scoring: check.email, batch.score, lists.add, config.set, label, forget, suppressions, events, stats. None of them belong in a browser, because all of them need your secret key. Full reference.

07

Test it

Terminal
npm run dev

Open http://localhost:5173, fill the form, submit, and look at the network tab. Your /api/signup request carries the id:

Output
{ "email": "you@example.com", "deviceId": "8f1c2ae9d4b7c3e05a1f6b28d9074e3c" }

Turn ad blockers off on localhost while you test. Any fingerprinting script can be blocked by an extension, and when that happens you get nothing rather than a cautious answer. We measured what that does to a competitor and published the numbers, including the part where our own endpoint is only unblocked because nobody has listed it yet. That is why the hook returns null instead of throwing, and why the device id is one signal in a score rather than the whole thing.

The other hooks

useSessionWatch: catch what one fingerprint cannot

useDeviceIdanswers "who is this" at one instant. Some of the most useful signals only exist across time, because they are a change rather than a value: a VPN that drops mid-session and leaks the real home IP, a session that starts masking partway through, one device seen from a dozen addresses. None of those can be read from a single page load.

src/AccountLayout.jsx
import { useSessionWatch } from "@kaidn/react";

export default function AccountLayout({ user, children }) {
  // Re-beacons the same device about once a minute while the tab is open,
  // so a connection change becomes visible. Stops on logout and unmount.
  useSessionWatch({ active: Boolean(user) });

  return <>{children}</>;
}

It costs nothing. The /v1/fp beacon is free and rate-limited; you are billed per scored decision. Watching a session adds signal without adding events, which is why it belongs on a logged-in layout rather than being rationed.

Consent

One flag on the provider stops every hook collecting anything:

src/main.jsx
<KaidnProvider publishableKey="pk_live_…" enabled={hasConsent}>

Fingerprinting reads properties of a visitor's device, which in several jurisdictions needs a lawful basis before it happens rather than a note in a policy afterwards. This library will not assume one on your behalf.

What this package will never do

Two hooks is the whole surface, and that is a boundary rather than a gap. The rest of the API, config, lists, label, forget, events, stats, batch, is reachable only with your secret key, and a secret key in a browser bundle is a leak found by whoever reads the bundle first. They would also be decisions a visitor could edit.

Every one of them lives in the server clients, which all cover the same surface:

score, scoreWithCookiejudge an event, and carry the device identity across visits
check.email / ip / phonejudge one identifier without recording an event
batch.*bulk, one quota unit per row, 1000 rows a call
lists.*your allow and blocklists, which beat every signal
config.get / setyour weights and thresholds
labelreport a real outcome: fraud, chargeback, legit
forget, suppressionsGDPR erasure, and the audit that makes it safe
events, statsread your own data back
graphSharingopt into the cross-operator graph

Full detail in the API reference.