Skip to main content

kaidn

Official Python client for Kaidn, the fraud and abuse scoring API.

Send one user action, get back allow, review or block, with the reasons attached.

pip install kaidn
from kaidn import KaidnClient

client = KaidnClient()                       # reads $KAIDN_API_KEY

r = client.score(event="signup", ip=ip, email=email)

if r.blocked:
    raise Denied(r.reason_text)

Zero runtime dependencies. This runs in your signup and checkout path, so every dependency it carried would be one more thing that can break your deploy or turn up in your vulnerability scanner. It uses the standard library and nothing else.

Requires Python 3.9+. Server-side only: it holds your secret key, so never ship it to a browser. The browser half is @kaidn/fp and uses a separate publishable key.

Score an event

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.

r = client.score(
    event="signup",
    user_id=user.id,
    ip=request.remote_addr,
    email=form["email"],
    device_id=form.get("kaidn_device_id"),   # from @kaidn/fp, if installed
)

r.verdict        # "allow" | "review" | "block"
r.reasons        # ["datacenter_ip", "disposable_email"]
r.reason_text    # a sentence you could send to the customer
r.score          # 0-100. Bookkeeping, not a probability: branch on the verdict

Branching, with the three cases people actually use:

if r.blocked:
    return deny()                     # generic message: a specific one teaches the next attempt
if r.needs_review:
    create_account(hold_rewards=True) # they can use the product, they just cannot earn yet
    flag_for_review(r.event_id, r.reason_text)
else:
    create_account()

Read the evidence

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.

for c in r.checks:
    print(c.reason, c.weight, c.key, c.evidence)
    # datacenter_ip 45 datacenterIp {'asn': '16509'}

Recognise a returning device

A browser fingerprint is not a person: on production traffic one iOS Safari fingerprint covers 2.30 different people. So use resolved_id, not id, and weigh it with collision_risk.

d = r.device
if d:
    d.resolved_id                  # the identity. Link visits on this
    d.collision_risk               # measured P(covers more than one person)
    d.account_count                # includes fingerprint collisions
    d.account_count_same_network   # the number you can defend to an angry user

Rung 1: stop guessing, start remembering

Pass the request's Cookie header and the client replays any token it finds. The identity comes back deterministic, and it survives the network change that splits a fingerprint-derived one in half.

from kaidn import CookieOptions, KaidnClient

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

r = client.score_with_cookie(
    event="login",
    ip=request.remote_addr,
    device_id=form.get("kaidn_device_id"),
    cookies=request.headers.get("Cookie"),
)

if r.set_cookie:
    response.headers["Set-Cookie"] = r.set_cookie

if r.device and r.device.resolution == "deterministic":
    ...   # we have seen THIS browser, not something that hashes like it

Measured on the same 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

Your server has to set the cookie, not us. Browsers judge a cookie by the domain that sent Set-Cookie, so one set from your backend on your own domain is genuinely first-party and lasts ~400 days. Anything a vendor sets from its own infrastructure is capped at 7 days on Safari, including the CNAME'd "custom subdomain" setups other vendors ask you to configure. No DNS record, no proxy.

It is off until you pass cookie=, deliberately. Storing something on a visitor's device needs consent or a strict-necessity basis under the ePrivacy Directive, and GDPR legitimate interest does not substitute for it. You are the controller here: the cookie is set by your server, on your domain, and belongs in your cookie policy. The token itself is opaque, carrying a random id, an issue time and a signature scoped to your account.

Dedupe one inbox, not one address

bob+1@gmail.com, b.o.b@gmail.com and bob@googlemail.com are one mailbox.

if r.identity and User.exists(email_canonical=r.identity.email_canonical):
    return reject("an account already uses this inbox")

Check an identifier on its own

No event recorded, useful at the form or when cleaning a list.

client.check.email("x9f2kq@mailinator.com").fraud_score   # 75
client.check.ip("3.5.140.1").report.get("is_datacenter")  # True
client.check.phone("+14155550123", country="US")

Report what really happened

Feedback is what sharpens scoring. legit marks your own false positive and never lowers anyone else's risk.

client.label(label="chargeback", event_id=r.event_id)
client.label(label="legit", event_id=r.event_id)

Everything the key can reach

If an endpoint takes an API key, it is a method here. No dropping back to raw HTTP for one call.

client.score(event=...) score one action
client.score_with_cookie(..., cookies=...) the same, carrying the device identity
client.check.email(...) .ip(...) .phone(...) judge one identifier, no event recorded
client.batch.score(rows) bulk, 1 quota unit per row, 1000 rows per call
client.batch.check.email(rows) .ip(rows) .phone(rows) bulk lookups
client.lists.list() .add(list, type, value) .remove(id) .import_(rows) allow / blocklists
client.config.get() .set(overrides) your weights and thresholds
client.label(label=..., event_id=...) report a real outcome
client.forget(email=...) GDPR erasure, local to your account
client.suppressions(limit=...) the audit of every forget and legit
client.events(...) client.stats(...) read your own data
client.graph_sharing(enabled) opt into the cross-operator graph
client.health() public liveness and intel dataset sizes

Runnable versions of all of it: examples/.

Errors

Everything raises KaidnError, with the API's own message.

from kaidn import KaidnError

try:
    r = client.score(event="signup", email=email)
except KaidnError as err:
    if err.status == 429:
        notify_ops("Kaidn quota exhausted")
    raise

Network failures, timeouts, 429s and 5xx are retried automatically (2 extra attempts by default, honouring Retry-After). A 4xx is not: a bad key fails identically the second time, and retrying it just spends quota and delays the error reaching whoever can fix it.

Set a timeout and fail open. A fraud vendor that can take down your signup form is a worse problem than the fraud:

try:
    r = client.score(event="signup", email=email)
except KaidnError:
    r = None          # create the account. Do not let our outage become yours.

Fields we have not named yet

Every response keeps what this version does not recognise, so a signal the API ships next week reaches code running the library you installed last year.

r.get("a_field_added_after_this_release")
r.device.get("some_new_signal")
r.extra                                    # everything unrecognised

Requests work the same way: any extra keyword to score() is passed through untouched.

Configuration

KaidnClient(
    api_key="kdn_live_...",   # default: $KAIDN_API_KEY
    base_url="https://api.kaidn.io",
    timeout=10.0,             # seconds per attempt
    retries=2,                # extra attempts on a transient failure
)

Links

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kaidn-1.1.0.tar.gz (16.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kaidn-1.1.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

Details for the file kaidn-1.1.0.tar.gz.

File metadata

  • Download URL: kaidn-1.1.0.tar.gz
  • Upload date:
  • Size: 16.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.12

File hashes

Hashes for kaidn-1.1.0.tar.gz
Algorithm Hash digest
SHA256 74bac31fb9445f0f110f0b30a7f4a744601f1051db9e97498e04f1e07cf2bd7d
MD5 bb87c92abe1fca9a982ed964a915e697
BLAKE2b-256 6976afe54d1795187d4913d24f88bb86cc8e6ac9fe636f17cbef617c18683fba

See more details on using hashes here.

File details

Details for the file kaidn-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: kaidn-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.12

File hashes

Hashes for kaidn-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6961c54e89d59ee11ee3e486d29a8604f67b97c8c9db1a24c386f5dde07a1df8
MD5 8b9686f13def85f54cb0457b35ab479c
BLAKE2b-256 8ee48fae9c5e790f0986d967baa746500b0e73bba0a87d54ff3d8beb8150aa41

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

1.0.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page