Metadata-Version: 2.5
Name: kaidn
Version: 1.0.0
Summary: Official Python client for Kaidn, the fraud and abuse scoring API.
Project-URL: Homepage, https://kaidn.io
Project-URL: Documentation, https://kaidn.io/docs
Project-URL: Source, https://github.com/Kaidn-io/kaidn-python
Project-URL: Issues, https://github.com/Kaidn-io/kaidn-python/issues
Author-email: Kaidn <support@kaidn.io>
License: MIT
License-File: LICENSE
Keywords: abuse,account-takeover,bot-detection,chargeback,device-fingerprinting,disposable-email,fraud,fraud-detection,fraud-prevention,ip-reputation,multi-accounting,proxy-detection,vpn-detection
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# kaidn

Official Python client for [Kaidn](https://kaidn.io), the fraud and abuse scoring API.

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

```bash
pip install kaidn
```

```python
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`](https://www.npmjs.com/package/@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.

```python
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:

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

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

```python
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
```

Store `r.device_token` as a first-party cookie on your own domain and pass it back as
`device_token` next time. The identity then becomes `deterministic`: remembered rather
than inferred.

## Dedupe one inbox, not one address

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

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

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

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

## Errors

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

```python
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:

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

```python
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

```python
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

- [Docs](https://kaidn.io/docs) · [Guides](https://kaidn.io/docs/guides) · [Glossary](https://kaidn.io/glossary)
- [Pricing](https://kaidn.io/pricing): 10,000 events a month free, no card

MIT
