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

  1. Create an account — you get an API key (shown once) and a dashboard.
  2. Send your first event to POST /v1/score with the x-api-key header.
  3. Branch on verdict: allow proceeds, block denies, review goes to a manual queue.
  4. 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:

credentialheaderused for
kdn_live_…x-api-keyserver-to-server scoring — keep it in your secrets manager
sess_…Authorization: Bearerdashboard 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

fieldtypenotes
eventstringrequired — your event name, 1–64 chars
user_idstringyour user's id — enables device-reuse and velocity tracking
ipstringIPv4 of the request — datacenter + velocity checks
emailstringchecked for disposable domains, gibberish, plus-tags, MX; only a hash is stored
device_idstringbrowser/device fingerprint (e.g. FingerprintJS visitorId) — the #1 multi-accounting signal
event_countrystringISO-3166 alpha-2 the event should come from
ip_countrystringISO country of the caller's IP — pass your CDN's header (CF-IPCountry etc.)

Response

fieldtypenotes
event_iduuidpersisted event id
score0–100summed check weights, clamped
verdictenumallow (<40) · review (40–69) · block (≥70) — thresholds tunable per tenant
reasonsstring[]machine-readable reason codes, in firing order
reason_textstringplain-English explanation (AI-written when a provider key is set, templated otherwise)
checksobject[]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.

codedefault weightfires when
datacenter_ip45IP sits in a known cloud/hosting CIDR range
proxy_ip / vpn_ip30IP flagged as proxy or VPN
device_reuse15 / 30 / 452+ / 4+ / 10+ accounts share the device fingerprint
ip_velocity25 / 405+ / 10+ accounts from one IP in 10 minutes
device_velocity2520+ events from one device in 10 minutes
disposable_email35email domain is on the disposable list (7,860 domains)
email_no_mx25email domain has no mail server
gibberish_email20mailbox name looks machine-generated
plus_addressing10email uses a +tag (duplicate-account tactic)
geo_mismatch20IP country ≠ expected event country
blocklisted_*100ip/email/device/user is on your blocklist — instant block
allowlisted_*0entity 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

keydefaultkeydefault
datacenterIp45emailDisposable35
proxyVpn30emailNoMx25
deviceReuseLow15emailGibberish20
deviceReuseMed30emailPlusTag10
deviceReuseHigh45geoMismatch20
ipVelocityMed25deviceVelocity25
ipVelocityHigh40

Defaults — thresholds

keydefaultmeaning
review40score ≥ this → verdict review
block70score ≥ this → verdict block
deviceReuseMed / High4 / 10account-count boundaries for device reuse tiers
ipVelocityMed / High5 / 10accounts-per-IP boundaries (10-minute window)
deviceVelocity20events-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

statusmeaningtypical cause
400invalid requestmissing event, unknown fields (strict validation), bad config keys
401unauthenticatedmissing/wrong API key, expired session, rotated key still in use
404not founddeleting a list entry or tenant that doesn't exist
409conflictregistering an email that already has an account
429rate limitedover 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.