Docs menuAll pages, quickstarts and this page’s contents

Guides

One job per guide, with code you can paste and run. These assume you have a key and have read the introduction; they do not assume you have read the reference.

Score a signup

The first integration almost everyone builds, and the one that pays for itself fastest, because a fake account is cheapest to stop before it exists.

Server, Node

signup.js
import { Kaidn } from "@kaidn/sdk";
const kaidn = new Kaidn({ apiKey: process.env.KAIDN_API_KEY! });

app.post("/signup", async (req, res) => {
  const r = await kaidn.score({
    event: "signup",
    ip: req.ip,
    email: req.body.email,
    // present only if you installed the browser tracker
    device_id: req.body.kaidn_device_id,
  });

  if (r.verdict === "block") {
    // Do not tell them which signal fired. 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") {
    // The account exists, it just does not earn yet.
    await flagForReview(user.id, r.event_id, r.reason_text);
  }

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

Server, Python

main.py
# pip install kaidn
from kaidn import KaidnClient, KaidnError

client = KaidnClient()          # reads $KAIDN_API_KEY

try:
    r = client.score(event="signup", ip=ip, email=email, device_id=device_id)
except KaidnError:
    # Fail OPEN. An outage in your fraud vendor must never become an
    # outage in your signup form.
    return create_account()

if r.blocked:
    # Do not name the signal. It teaches the next attempt.
    return deny("We could not create that account.")

user = create_account()

if r.needs_review:
    # The account exists, it just does not earn yet.
    flag_for_review(user.id, r.event_id, r.reason_text)

What to do with each verdict

verdicta good defaultwhy
allowcreate the account normallyno signal worth acting on
reviewcreate the account, withhold what is worth stealingif you are wrong, a real user notices nothing. If you are right, you removed the incentive without an appeals queue
blockrefuse, with a generic messagea specific error message is a free debugging tool for the next attempt

Set a timeout and fail open. Three seconds is generous. If Kaidn is slow or unreachable, create the account: an outage in your fraud vendor must never become an outage in your signup form.

Recognise a returning device

A browser fingerprint alone cannot reliably tell a returning visitor from a stranger with the same model of phone. The fix is to stop guessing: store a token we issue, send it back next time, and the identity is remembered rather than inferred.

The SDK handles the cookie for you. Note it is off unless you pass cookie, because storing something on a visitor's device is your decision and belongs in your cookie policy, not ours.

Express

signup.js
const kaidn = new Kaidn({ apiKey: process.env.KAIDN_API_KEY!, cookie: {} });

app.post("/signup", async (req, res) => {
  const r = await kaidn.scoreWithCookie(
    { event: "signup", ip: req.ip, email: req.body.email,
      device_id: req.body.kaidn_device_id },
    { cookies: req.headers.cookie,
      setCookie: (h) => res.append("Set-Cookie", h) },
  );

  if (r.device?.resolution === "deterministic") {
    // We have seen this exact browser before and we know it.
    // This is the strongest device signal available.
  }
});

Anything that is not Express

code
const r = await kaidn.scoreWithCookie(event, { cookies: cookieHeader });
reply.header("Set-Cookie", r.set_cookie);

Check that it worked

On the second visit from the same browser, device.resolution should read deterministic. If it still says probabilistic, look at device.token_rejected: it is set when a token arrived but could not be used, and it is almost always a cookie that is not being sent back rather than an attacker.

Your server has to set the cookie, not us. A cookie set by your backend on your own domain is genuinely first-party and lasts around 400 days. Anything a vendor sets from its own infrastructure is capped at 7 days on Safari, including the CNAME arrangements other vendors ask you to configure. There is no DNS record to add here.

Guard a cashout or withdrawal

The signup is where fraud starts; the cashout is where it costs you. Scoring only at signup means an account that looked fine in January can take money in March.

At the moment value leaves

cashout.js
const r = await kaidn.score({
  event: "cashout",
  user_id: user.id,          // links this to everything else they have done
  ip: req.ip,
  email: user.email,
  device_id: req.body.kaidn_device_id,
});

if (r.verdict === "block")  return hold(payout, r.reason_text);
if (r.verdict === "review") return queueForApproval(payout, r.event_id);

return pay(payout);

Two things make this call sharper than the signup call. Passing user_id lets Kaidn connect this request to every other event that account has produced. And by now the account has history, so reuse and speed signals have something to work with that they did not have on day one.

Hold, do not delete. A withheld payout can be released in a minute when you are wrong. A closed account and a confiscated balance produce a support ticket, a chargeback, and a review that will outlive the fraud you prevented.

Catch account takeover at login

A stolen account is a real account, so the email is genuine and the history is clean. What gives it away is change: the login arrives from a device and a network that have never been near this user before.

On successful password check, before you issue a session

login.js
const r = await kaidn.scoreWithCookie(
  { event: "login", user_id: user.id, ip: req.ip,
    email: user.email, device_id: req.body.kaidn_device_id },
  { cookies: req.headers.cookie,
    setCookie: (h) => res.append("Set-Cookie", h) },
);

// A recognised device is the single most useful thing here.
const knownDevice = r.device?.resolution === "deterministic";

if (r.verdict !== "allow" && !knownDevice) {
  return requireSecondFactor(user, r.event_id);
}

This is the guide where the device token earns its keep. Without it you are asking whether the fingerprint looks familiar; with it you are asking whether this is the same browser, and the answer is yes or no.

Step up, do not lock out.The correct response to a suspicious login is a second factor, not a closed door. People do travel, replace phones, and use their partner's laptop.

Stop one person holding many accounts

The hardest category, because you usually cannot prove it. You can establish that two accounts are strongly related. Whether that is one person or two flatmates sharing a laptop is an inference, and any approach that forgets this will eventually ban somebody's mother.

Dedupe the inbox, not the address

The cheapest win, and it needs no device data at all. Every scored event returns a canonical form of the email, with plus-tags, dots and provider aliases collapsed.

signup.js
// bob+1@gmail.com, b.o.b@gmail.com and bob@googlemail.com
// all return the same canonical value
const canonical = r.identity?.email_canonical;

if (canonical && await db.users.exists({ emailCanonical: canonical })) {
  return reject("an account already uses this inbox");
}

// store BOTH: mail the address they typed, dedupe on the canonical one
await db.users.create({ email: typed, emailCanonical: canonical });

Use the device count you can defend

The response carries two counts and they are not equally trustworthy.

fieldwhat it countstrust
account_countaccounts on this raw fingerprintincludes collisions. On iOS Safari one fingerprint covers 2.30 different people
account_count_same_networkaccounts on this fingerprint AND this networkthe number you can defend to an angry customer

Never hard-ban on a shared device fingerprint. Families, flatmates, libraries, internet cafés and entire markets where a shared machine is normal all look identical to a fraud ring if you only count devices. Withhold the thing that made the second account worth creating instead: do not pay the referral, do not grant the second trial, do not count the second entry.

Tune it to your traffic

Defaults are a starting point, not an answer. The useful loop is: run in observe-only for a week, read what fired, then move the two or three weights that are wrong for you.

  1. Score everything, act on nothing. Send events and log the verdict without branching on it. You now have your own traffic to argue with.
  2. Read the reason mix. /v1/stats gives you the top reason codes for a window. A code that fires on almost everything is either your biggest problem or your biggest false positive, and you can usually tell which by looking at ten of them.
  3. Move one weight at a time. PUT /v1/config stores only what you change; everything else keeps inheriting the tuned defaults.
  4. Report real outcomes. When a chargeback lands or you confirm a ring by hand, say so with POST /v1/label. A legitlabel marks your own false positive and never lowers anyone else's risk.

Mobile-heavy traffic needs looser speed thresholds than you think. Carrier networks put thousands of unrelated people behind one address, so a per-IP threshold tuned on desktop traffic will flag a city.

Backfill a CSV

Useful before you go live: score the users you already have, so the first day of real traffic is not also the first day the engine has ever seen your data.

Batch, up to 1000 rows a call

code
const { summary, results } = await kaidn.batch.score(rows);
console.log(`${summary.block} blocked of ${summary.total}`);

Each row costs one event of quota, the same as a live call. The dashboard has the same thing as a file upload if you would rather not write the loop.

Read the result before you act on it.A backfill judges history with today's rules, and old rows are missing the device and connection data live events carry, so the verdict mix will look harsher and thinner than reality. Treat it as a list of accounts worth looking at, not a list of accounts to close.

Investigate by asking, instead of writing queries

Connect Claude Code, Claude Desktop, Cursor or any MCP client to your tenant and investigate in plain English. The server is a thin client over this API, it holds no judgment of its own, so what an agent sees is exactly what your own calls return, evidence included.

claude mcp add kaidn \
  --env KAIDN_API_KEY=your_key \
  -- npx -y @kaidn/mcp

# then just ask:
#   "why was this signup blocked?"
#   "show me everything that touched this device"
#   "are these two accounts the same person?"

Read-only by default. Ten tools are registered: get_stats, list_events, explain_event, triage_queue, get_config, investigate_entity, check_email, check_ip, check_phone and score_event. Passing --allow-writes adds add_to_list and label_outcome. Editing config and GDPR erasure are deliberately not exposed in any mode, both belong in a dashboard where a person can see what they are about to do.

The free tools cost nothing; enrichment and scoring each spend one row of quota, and a per-process ceiling (KAIDN_MCP_MAX_QUOTA_CALLS, default 100) stops an agent loop from spending your month unattended. A reserve that would overshoot the ceiling is refused outright rather than partially spent.

variabledefaultnotes
KAIDN_API_KEYrequiredyour secret key: read from the environment only, never a tool argument
KAIDN_API_URLapi.kaidn.iooverride the API base URL
KAIDN_MCP_ALLOW_WRITESunset1 is equivalent to --allow-writes
KAIDN_MCP_MAX_QUOTA_CALLS100ceiling on quota-consuming calls per process

Tell your users (and stay compliant)

You are the controllerfor your end users’ data. We are your processor. That means the duty to tell people what happens to their data is yours, and we cannot discharge it for you — but we can make it a paste rather than a research project.

Read this before you ship the browser SDK in the EU. Device fingerprinting reads information from the visitor’s device, which ePrivacy Art. 5(3) treats as requiring consent— and legitimate interest is not an available basis for it. KaidnProvider defaults to enabled={true}, so if you are collecting from EEA, UK or Swiss visitors you should pass your own consent state instead:

Gate collection on consent

code
<KaidnProvider publishableKey="pk_live_…" enabled={hasConsent}>
  <App />
</KaidnProvider>

Server-side scoring is different: POST /v1/score reads nothing from the device, so it rests on your legitimate interest in preventing fraud (Recital 47 names it explicitly). The consent question applies to the browser collection, not to scoring.

Wording you can paste into your privacy policy

Written to be accurate about what we actually do. Adapt the wording, but do not make it vaguer — “we use security tools” is not a disclosure.

Fraud prevention section

code
### Fraud and abuse prevention

We use Kaidn, a fraud-detection service, to check signups, logins and
transactions for signs of fraud and abuse.

What is checked. Your IP address, a one-way hash of your email address and
of your phone number, the email domain, a device identifier, the type of
event and when it happened. Kaidn stores hashes rather than your address
or number.

Why. To protect this service and its users from fraudulent accounts,
payment abuse and automated attacks. Our lawful basis is our legitimate
interest in preventing fraud (Art. 6(1)(f)); fraud prevention is
recognised as a legitimate interest in Recital 47 of the GDPR.

Where. Data is processed on servers in Germany.

How long. Records are deleted automatically after [30/60/90/180] days.

Automated decisions. Kaidn returns a risk score and a recommendation. [We
review flagged cases before acting / An account may be automatically
suspended pending review.] You can ask us to review any decision, express
your point of view, and contest the outcome — contact us at [email].

Your rights. You can ask us for a copy of what is held about you, or ask
us to delete it. Contact [email] and we will action it, including with
our processor.

If you act on a verdict automatically

Blocking an account or holding a payout on a score alone, with no human involved, is an automated decision under Art. 22if it has a legal or similarly significant effect on the person — and denying someone access or money usually does. That triggers obligations on you:

ArticleWhat you have to provide
22(3)A route to human intervention — a person who can look at it
22(3)A way for the individual to express their point of view
22(3)A way to contest the decision
13(2)(f)Tell them, at collection, that automated decision-making happens
15(1)(h)On request, meaningful information about the logic involved

Two things make this easier than it sounds. Every verdict comes with reasons and a plain-English reason_text, which is the “meaningful information about the logic” Art. 15(1)(h) asks for — you can show it to the person. And a verdict of review is not an automated decision at all, because a human makes the call. Routing significant actions through review rather than block keeps you outside Art. 22 entirely.

One caveat we would rather you heard from us.Device recognition is probabilistic. On some platforms a single device identifier can cover more than one person — iOS Safari is the worst case. Treat a device match as evidence, not proof, and never let it block on its own. Our engine already refuses to let a weak device key drive a cross-operator flag for the same reason.

Answering a data subject request

You have 30 days (Art. 12(3)). Both of these are one call, no ticket with us:

Everything held about one person (Art. 15 / 20)

code
curl -X POST https://api.kaidn.io/v1/subject \
  -H "x-api-key: $KAIDN_KEY" -H "content-type: application/json" \
  -d '{"email":"person@example.com"}'

Erase them (Art. 17)

code
curl -X POST https://api.kaidn.io/v1/forget \
  -H "x-api-key: $KAIDN_KEY" -H "content-type: application/json" \
  -d '{"email":"person@example.com"}'

Both accept email, phone, ip, device_id or user_id, and both match email aliases — erasing a@gmail.com also reaches rows written for a+tag@googlemail.com. What /v1/subject shows is exactly what /v1/forget removes.

Our Data Processing Agreement is already in force — nothing to request or sign — and our sub-processor list tells you who else touches the data and where.

This is engineering guidance from one operator to another, not legal advice. Your circumstances and your regulator are yours.