> Source: https://kaidn.io/docs/quickstart/python
> 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 API key](#step-1)
- [2. Set up your project](#step-2)
- [3. Score your first signup](#step-3)
- [4. Block bots and datacenter traffic](#step-4)
- [5. Stop one person opening many accounts](#step-5)
- [6. Test it](#step-6)
- [Next steps](#next)

# Python server quickstart

Add Kaidn to a Python backend and have it blocking real abuse in about fifteen minutes. By the end you will have an endpoint that scores every signup, refuses datacenter and bot traffic, and catches one person opening a second account under a different spelling of the same inbox.

Examples use **FastAPI** because it is the common default for a new Python service. Nothing here depends on it: the client is a plain object and the same four lines work in Django, Flask or a worker with no web framework at all.

### Before you start

- **Python 3.9 or later.** Nothing newer is required.
- **A free Kaidn account.** [Register](https://kaidn.io/register): 10,000 events a month, no card.
- **Optional but worth it: the browser tracker.** Without it Kaidn scores the IP, email and phone. With it you also get the device signals, which is what makes steps 4 and 5 sharp. See [installing the tracker](https://kaidn.io/docs/keys#tracker).

## 1. Get your API key

Create an account and your first key appears immediately. There is no sales call and no card. **The key is shown once**, so copy it now.

Put it in a `.env` file, never in your source:

.env

```
# .env
KAIDN_API_KEY=kdn_live_your_secret_key_here
```

**Two kinds of key, and only one belongs on a server.** The secret key (`kdn_live_…`) scores events and reads your data. The publishable tracker key (`pk_live_…`) is domain-locked, can only send fingerprints, and is meant to be visible in page source. Passing a `pk_` key to this client raises immediately with an explanation, rather than letting it fail later as an opaque 401.

## 2. Set up your project

shell

```
mkdir kaidn-quickstart && cd kaidn-quickstart
python -m venv .venv && source .venv/bin/activate

pip install kaidn fastapi "uvicorn[standard]" python-dotenv
```

`kaidn` has **no dependencies of its own**. Everything else on that line is your web server, not ours. This runs in your signup path, so every dependency it carried would be one more thing that can break your deploy or turn up in your vulnerability scanner.

Create `main.py`:

main.py

```
import os
from dotenv import load_dotenv
from fastapi import FastAPI, Request, Response
from kaidn import KaidnClient, KaidnError

load_dotenv()

# Reads $KAIDN_API_KEY. Construct it once, at import, not per request.
client = KaidnClient()
app = FastAPI()
```

## 3. Score your first signup

One call. `event` is the only required field and the name is yours to choose. Send whatever else you already collect: the answer sharpens as you send more, and nothing is mandatory.

main.py

```
@app.post("/signup")
async def signup(request: Request, response: Response):
    body = await request.json()

    try:
        r = client.score(
            event="signup",
            ip=request.client.host,
            email=body["email"],
            device_id=body.get("kaidn_device_id"),   # from the browser tracker
        )
    except KaidnError:
        # FAIL OPEN. An outage in your fraud vendor must never become an
        # outage in your signup form.
        return {"ok": True}

    return {"verdict": r.verdict, "why": r.reason_text}
```

Run it, and post a signup:

shell

```
uvicorn main:app --reload

curl -X POST localhost:8000/signup -H 'content-type: application/json' \
  -d '{"email":"bob@gmail.com"}'
```

You get one of three answers back, and **you** decide what each one means. Kaidn never blocks anybody on your behalf.

| verdict | a sensible default | why |
| --- | --- | --- |
| `r.verdict == "allow"` | create the account | nothing worth acting on |
| `r.needs_review` | create it, withhold what is worth stealing | if you are wrong a real user notices nothing; if you are right you removed the incentive without an appeals queue |
| `r.blocked` | refuse, with a generic message | a specific error is a free debugging tool for the next attempt |

**Always set a timeout and fail open.** The client already retries a network failure, a 429 and a 5xx twice, honouring `Retry-After`, and never retries a 4xx because a bad key fails identically the second time. What it cannot decide for you is what to do when Kaidn is unreachable, and the answer is almost always: let the signup through.

## 4. Block bots and datacenter traffic

Real customers browse from a home ISP or a mobile carrier. A signup arriving from AWS is usually a script. Branch on the **reason codes** rather than the score: they are stable strings, and they say what was actually found.

main.py

```
    if r.blocked:
        response.status_code = 403
        # Do not name the signal. It teaches the next attempt.
        return {"error": "We could not create that account."}

    # Or act on one specific finding, whatever the total came to:
    if "datacenter_ip" in r.reasons or "headless_browser" in r.reasons:
        response.status_code = 403
        return {"error": "We could not create that account."}
```

Every verdict shows its work. `key` is the config key you would edit to retune that check, so a decision tells you how to change it next time:

main.py

```
for c in r.checks:
    print(c.reason, c.weight, c.key, c.evidence)

# abusive_asn      35  abusiveAsn    {'asn': '16509'}
# datacenter_ip    45  datacenterIp  {'asn': '16509'}
# plus_addressing  10  emailPlusTag  {}
```

The full list of codes is in the [reference](https://kaidn.io/docs/api#reasons), grouped by what they look at.

## 5. Stop one person opening many accounts

The hardest category, and the one most likely to make you punish a real customer. Two things do most of the work, and neither is a device ban.

#### Dedupe the inbox, not the address

`bob@gmail.com`, `b.o.b@gmail.com` and `bob+promo@googlemail.com`are one mailbox. Every scored event returns the canonical form, so you dedupe on that instead of the address they typed, without encoding any provider's rules yourself.

main.py

```
import sqlite3

db = sqlite3.connect("users.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS users (email TEXT, canonical TEXT)")

    # ...inside the handler, after scoring:
    canonical = r.identity.email_canonical if r.identity else body["email"]

    if db.execute("SELECT 1 FROM users WHERE canonical=?", (canonical,)).fetchone():
        response.status_code = 409
        return {"error": "An account already uses this inbox."}

    # Store BOTH: mail the address they typed, dedupe on the canonical one.
    db.execute("INSERT INTO users VALUES (?,?)", (body["email"], canonical))
    db.commit()
```

Which does this, verified against the live API:

output

```
POST /signup  {"email": "bob@gmail.com"}
  -> 200  {"ok": true}

POST /signup  {"email": "b.o.b+promo@googlemail.com"}
  -> 409  {"error": "An account already uses this inbox."}

sqlite> SELECT email, canonical FROM users;
bob@gmail.com | bob@gmail.com          <- one row, not two
```

**The dot trick is a Gmail behaviour, not a universal one.** On a provider that treats dots as significant, `a.b@x.com` and `ab@x.com` really are two different mailboxes, and the canonical form correctly keeps them apart. This is why you take the canonical from the API rather than writing the rules yourself.

#### Use the device count you can defend

A browser fingerprint is not a person. On production traffic **one iOS Safari fingerprint covers 2.30 different people**, because a default iPhone is identical to another default iPhone. So the response gives you two counts, and they are not equally trustworthy.

| field | counts | trust |
| --- | --- | --- |
| `device.account_count` | accounts on the raw fingerprint | includes collisions |
| `device.account_count_same_network` | accounts on that fingerprint AND that network | the number you can defend to an angry customer |

**Never hard-ban on a shared device.** Families, flatmates, libraries, internet cafés and whole markets where a shared machine is normal all look exactly like 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.

#### Recognise the browser properly: rung 1

Everything above still identifies a browser by inference. Store the token Kaidn returns as a cookie on your own domain and the next visit is *remembered* instead:

Needs `kaidn` 1.1.0 or later: `pip install -U kaidn`.

main.py

```
from kaidn import CookieOptions

client = KaidnClient(cookie=CookieOptions())   # off unless you ask

    r = client.score_with_cookie(
        event="signup",
        ip=request.client.host,
        email=body["email"],
        device_id=body.get("kaidn_device_id"),
        cookies=request.headers.get("cookie"),
    )
    if r.set_cookie:
        response.headers["set-cookie"] = r.set_cookie
```

Measured on one browser across two visits, with the IP changed in between:

| | visit 1 | visit 2 |
| --- | --- | --- |
| `resolution` | probabilistic | **deterministic** |
| `resolution_rung` | 2 | **1** |
| `collision_risk` | 0.12 | **0.01** |

A network change is exactly what splits a fingerprint-derived identity in half. The token does not care. It is off unless you pass `cookie=`, because storing something on a visitor's device needs consent or a strict-necessity basis under ePrivacy, and that is your call rather than a library's. [The full reasoning](https://kaidn.io/docs/concepts#device-token).

## 6. Test it

Three inputs that should give three different answers. If they do, you are integrated.

shell

```
# 1. clean signup -> allow
curl -X POST localhost:8000/signup -H 'content-type: application/json' \
  -d '{"email":"real.person@outlook.com"}'

# 2. disposable inbox -> the reasons name it
curl -X POST localhost:8000/signup -H 'content-type: application/json' \
  -d '{"email":"x9f2kq@mailinator.com"}'

# 3. the same inbox again, respelled -> 409 from YOUR check
curl -X POST localhost:8000/signup -H 'content-type: application/json' \
  -d '{"email":"real.person+promo@outlook.com"}'
```

Then open the [dashboard](https://kaidn.io/app): every call you just made is there with the checks that fired, their weights and the raw evidence. That screen is the one that matters in an argument, because when a customer asks why they were treated unfairly you answer with a fact rather than a score.

**Score the cashout too, not just the signup.** Signup is where fraud starts; cashout is where it costs you. An account that looked fine in January can take money in March, and by then it has history for the reuse and velocity signals to work with.

## Next steps

[Guard a cashoutThe money path, and the one most relevant if you pay people](https://kaidn.io/docs/guides#cashout)[Catch account takeoverWhat changes between a normal login and a stolen one](https://kaidn.io/docs/guides#ato)[Core conceptsHow a verdict is made, and where the engine fails](https://kaidn.io/docs/concepts)[API referenceEvery endpoint, field and reason code](https://kaidn.io/docs/api)

The client covers every endpoint an API key can reach, and each one has a runnable example in [the repository](https://github.com/Kaidn-io/kaidn-python/tree/main/examples). Something here that did not work? [support@kaidn.io](mailto:support@kaidn.io). A quickstart whose examples fail is a bug, and worth reporting like one.
