API reference
Base URL: https://api.kaidn.dev (local development: http://localhost:3001). All requests and responses are JSON. Unknown request fields are rejected, not ignored.
Quickstart
- Create an account — you get an API key (shown once) and a dashboard.
- Send your first event to
POST /v1/scorewith thex-api-keyheader. - Branch on
verdict:allowproceeds,blockdenies,reviewgoes to a manual queue. - Watch events land in the dashboard, tune rules as you learn your traffic.
curl -X POST https://api.kaidn.dev/v1/score \
-H "x-api-key: $KAIDN_API_KEY" \
-H "content-type: application/json" \
-d '{
"event": "cashout",
"user_id": "u_8231",
"ip": "3.5.140.1",
"email": "x9f2kq@mailinator.com",
"device_id": "fp_c0ffee"
}'Authentication
Two credentials, two purposes:
| credential | header | used for |
|---|---|---|
kdn_live_… | x-api-key | server-to-server scoring — keep it in your secrets manager |
sess_… | Authorization: Bearer | dashboard sessions from login — 30-day expiry |
Only hashes of both are stored. A lost API key can't be recovered — rotate it (from Settings or POST /auth/rotate-key); the old key stops working immediately.
# create an account (returns session token + your API key — shown once)
curl -X POST https://api.kaidn.dev/auth/register \
-H "content-type: application/json" \
-d '{"email": "you@company.io", "password": "correct-horse-battery",
"company": "Acme Rewards"}'
# log in
curl -X POST https://api.kaidn.dev/auth/login \
-H "content-type: application/json" \
-d '{"email": "you@company.io", "password": "correct-horse-battery"}'
# rotate your API key (session required; old key dies instantly)
curl -X POST https://api.kaidn.dev/auth/rotate-key \
-H "Authorization: Bearer $SESSION_TOKEN"POST /v1/score
Scores one event about one user. The engine is event-agnostic — use whatever event names fit your product (signup, trial_start, cashout, credit_redeem, …). History accumulates per tenant: device reuse and velocity signals get sharper with every event you send.
Request body
| field | type | notes |
|---|---|---|
event | string | required — your event name, 1–64 chars |
user_id | string | your user's id — enables device-reuse and velocity tracking |
ip | string | IPv4 of the request — datacenter + velocity checks |
email | string | checked for disposable domains, gibberish, plus-tags, MX; only a hash is stored |
device_id | string | browser/device fingerprint (e.g. FingerprintJS visitorId) — the #1 multi-accounting signal |
event_country | string | ISO-3166 alpha-2 the event should come from |
ip_country | string | ISO country of the caller's IP — pass your CDN's header (CF-IPCountry etc.) |
Response
| field | type | notes |
|---|---|---|
event_id | uuid | persisted event id |
score | 0–100 | summed check weights, clamped |
verdict | enum | allow (<40) · review (40–69) · block (≥70) — thresholds tunable per tenant |
reasons | string[] | machine-readable reason codes, in firing order |
reason_text | string | plain-English explanation (AI-written when a provider key is set, templated otherwise) |
checks | object[] | full detail per fired check: weight, reason, message, raw evidence |
{
"event_id": "719d4bb6-5083-41d9-852e-9331f8f703e4",
"score": 95,
"verdict": "block",
"reasons": ["datacenter_ip", "device_reuse", "disposable_email"],
"reason_text": "Strong indicators of abuse: the IP is a hosting provider
address (amazon), this device is already linked to 3 accounts, and the
email uses a disposable domain. Risk score 95/100 → block.",
"checks": [
{
"check": "ipRisk",
"weight": 45,
"reason": "datacenter_ip",
"message": "IP is a datacenter/hosting address, not a residential user",
"evidence": { "asn": "amazon" }
},
{
"check": "deviceReuse",
"weight": 15,
"reason": "device_reuse",
"message": "This device is linked to 3 accounts",
"evidence": { "accountCount": 3 }
},
{
"check": "emailRisk",
"weight": 35,
"reason": "disposable_email",
"message": "Email uses a disposable/temporary domain",
"evidence": {}
}
]
}Reason codes
Every code is stable and machine-matchable; every firing carries an evidence object with the raw numbers.
| code | default weight | fires when |
|---|---|---|
datacenter_ip | 45 | IP sits in a known cloud/hosting CIDR range |
proxy_ip / vpn_ip | 30 | IP flagged as proxy or VPN |
device_reuse | 15 / 30 / 45 | 2+ / 4+ / 10+ accounts share the device fingerprint |
ip_velocity | 25 / 40 | 5+ / 10+ accounts from one IP in 10 minutes |
device_velocity | 25 | 20+ events from one device in 10 minutes |
disposable_email | 35 | email domain is on the disposable list (7,860 domains) |
email_no_mx | 25 | email domain has no mail server |
gibberish_email | 20 | mailbox name looks machine-generated |
plus_addressing | 10 | email uses a +tag (duplicate-account tactic) |
geo_mismatch | 20 | IP country ≠ expected event country |
blocklisted_* | 100 | ip/email/device/user is on your blocklist — instant block |
allowlisted_* | 0 | entity is on your allowlist — instant allow |
GET /v1/events · GET /v1/stats
Everything you score is queryable. /v1/events returns newest-first (query: limit ≤ 200, offset, verdict, event). /v1/stats aggregates a time window (query: window_hours, default 24): totals by verdict, average score, and the top reason codes. Raw email addresses are never stored — you get emailHash and emailDomain.
curl "https://api.kaidn.dev/v1/events?limit=50&verdict=block" \ -H "x-api-key: $KAIDN_API_KEY" curl "https://api.kaidn.dev/v1/stats?window_hours=168" \ -H "x-api-key: $KAIDN_API_KEY"
Custom rules — GET/PUT /v1/config
Every weight and threshold is overridable per tenant. You store only what you change; everything else keeps inheriting Kaidn's tuned defaults. Unknown keys and out-of-range values (outside 0–100) are rejected.
# read current rules (your overrides + effective config)
curl https://api.kaidn.dev/v1/config -H "x-api-key: $KAIDN_API_KEY"
# override any weight or threshold — everything else keeps the defaults
curl -X PUT https://api.kaidn.dev/v1/config \
-H "x-api-key: $KAIDN_API_KEY" \
-H "content-type: application/json" \
-d '{
"weights": { "emailDisposable": 80 },
"thresholds": { "review": 30, "block": 60 }
}'Defaults — weights
| key | default | key | default |
|---|---|---|---|
datacenterIp | 45 | emailDisposable | 35 |
proxyVpn | 30 | emailNoMx | 25 |
deviceReuseLow | 15 | emailGibberish | 20 |
deviceReuseMed | 30 | emailPlusTag | 10 |
deviceReuseHigh | 45 | geoMismatch | 20 |
ipVelocityMed | 25 | deviceVelocity | 25 |
ipVelocityHigh | 40 |
Defaults — thresholds
| key | default | meaning |
|---|---|---|
review | 40 | score ≥ this → verdict review |
block | 70 | score ≥ this → verdict block |
deviceReuseMed / High | 4 / 10 | account-count boundaries for device reuse tiers |
ipVelocityMed / High | 5 / 10 | accounts-per-IP boundaries (10-minute window) |
deviceVelocity | 20 | events-per-device boundary (10-minute window) |
Allow / blocklists — /v1/lists
Blocklisted entities score 100/block instantly; allowlisted ones always pass. Blocks beat allows when both match. Types: ip, email, device, user. Endpoints: GET /v1/lists, POST /v1/lists, DELETE /v1/lists/:id.
# block a device everywhere, instantly (score 100 / block)
curl -X POST https://api.kaidn.dev/v1/lists \
-H "x-api-key: $KAIDN_API_KEY" \
-H "content-type: application/json" \
-d '{"list": "block", "type": "device", "value": "fp_c0ffee"}'
# allowlist a trusted user (always passes)
curl -X POST https://api.kaidn.dev/v1/lists \
-H "x-api-key: $KAIDN_API_KEY" \
-H "content-type: application/json" \
-d '{"list": "allow", "type": "user", "value": "u_vip_1"}'
curl https://api.kaidn.dev/v1/lists -H "x-api-key: $KAIDN_API_KEY"
curl -X DELETE https://api.kaidn.dev/v1/lists/42 -H "x-api-key: $KAIDN_API_KEY"Free checker — POST /v1/check
Public, no API key. Deep-inspects an email and/or IP without scoring history: full MX record list, DNS/SPF/DMARC, domain age (RDAP), disposable/role/gibberish flags; IP geolocation, ISP/org/ASN, reverse hostname, proxy/hosting/Tor/datacenter flags — each with its own 0–100 fraud score. Fields that can't be resolved come back null, never an error.
# no API key needed — public endpoints
# both at once
curl -X POST https://api.kaidn.dev/v1/check \
-H "content-type: application/json" \
-d '{"email": "x9f2kq@mailinator.com", "ip": "3.5.140.1"}'
# email only
curl -X POST https://api.kaidn.dev/v1/check/email \
-H "content-type: application/json" \
-d '{"email": "x9f2kq@mailinator.com"}'
# ip only
curl -X POST https://api.kaidn.dev/v1/check/ip \
-H "content-type: application/json" \
-d '{"ip": "3.5.140.1"}'Errors
| status | meaning | typical cause |
|---|---|---|
| 400 | invalid request | missing event, unknown fields (strict validation), bad config keys |
| 401 | unauthenticated | missing/wrong API key, expired session, rotated key still in use |
| 404 | not found | deleting a list entry or tenant that doesn't exist |
| 409 | conflict | registering an email that already has an account |
| 429 | rate limited | over the per-key request budget — back off and retry |
Error bodies are { "error": "human-readable message" }. Scoring itself fails safe: if the AI narration layer is down you still get a templated reason_text; a score is never blocked on an LLM.
Rate limits
240 requests/minute per API key (429 above that, with standard rate-limit headers). The public /v1/check endpoint is limited per IP. Higher limits come with the Scale plan.
Something missing? Create a free account and poke the API directly — every example on this page runs against your own tenant unchanged.