kaidn/kaidn-php

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

Maintainers

Package info

github.com/Kaidn-io/kaidn-php

Homepage

Documentation

pkg:composer/kaidn/kaidn-php

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-21 22:47 UTC

This package is auto-updated.

Last update: 2026-08-22 13:30:58 UTC


README

Kaidn

Packagist version PHP version tests license MIT docs

kaidn/kaidn-php

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

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

composer require kaidn/kaidn-php
use Kaidn\Client;

$client = new Client();                      // reads KAIDN_API_KEY

$r = $client->score([
    'event' => 'signup',
    'ip'    => $_SERVER['REMOTE_ADDR'],
    'email' => $email,
]);

if ($r->isBlocked()) {
    throw new Denied($r->reason_text);
}

No Composer dependencies. This runs in your signup and checkout path, so every package it pulled in would be one more thing that can break your deploy or appear in your audit. It uses ext-curl, which is on effectively every PHP host including shared cPanel.

PHP 7.4 and up, deliberately. 7.4 is long past end of life upstream and it is what a lot of the rewards and affiliate shops using this actually run: one live site on Kaidn today is on 7.4.33. A client that refuses to install is worse than one written without enums and constructor promotion.

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

Score an event

event is the only required key, 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'        => $_SERVER['REMOTE_ADDR'],
    'email'     => $_POST['email'],
    'device_id' => $_POST['kaidn_device_id'] ?? null,   // from @kaidn/fp
]);

$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

The three cases people actually use:

if ($r->isBlocked()) {
    return deny();                      // generic message: a specific one teaches the next attempt
}
if ($r->needsReview()) {
    createAccount($holdRewards = true); // they can use the product, they just cannot earn yet
    flagForReview($r->event_id, $r->reason_text);
} else {
    createAccount();
}

Read the evidence

Every verdict shows its work. key is the config key you would edit to retune that check.

foreach ($r->checks as $c) {
    echo "$c->reason $c->weight $c->key " . json_encode($c->evidence) . "\n";
    // datacenter_ip 45 datacenterIp {"asn":"16509"}
}

Recognise a returning device: rung 1

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

Better still, stop guessing. Store the token Kaidn returns as a cookie on your own domain and pass the request's Cookie header back next time:

use Kaidn\Client;
use Kaidn\CookieOptions;

$client = new Client(null, ['cookie' => new CookieOptions()]);   // off unless you ask

$r = $client->scoreWithCookie(
    ['event' => 'login', 'ip' => $_SERVER['REMOTE_ADDR'], 'email' => $email],
    $_SERVER['HTTP_COOKIE'] ?? null
);

if ($r->set_cookie !== null) {
    header('Set-Cookie: ' . $r->set_cookie, false);
}

if ($r->device && $r->device->resolution === 'deterministic') {
    // we have seen THIS browser, not something that hashes like it
}

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

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.

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 && User::existsByCanonical($r->identity->email_canonical)) {
    return reject('an account already uses this inbox');
}

Store both: mail the address they typed, dedupe on the canonical one.

Everything the key can reach

If an endpoint takes an API key, it is a method here.

score($event) score one action
scoreWithCookie($event, $cookieHeader) the same, carrying the device identity
checkEmail() checkIp() checkPhone() judge one identifier, no event recorded
batchScore() batchCheck() batchLists() bulk, 1 quota unit per row, 1000 per call
lists() listAdd() listRemove() allow / blocklists
config() setConfig() your weights and thresholds
label() report a real outcome
forget() suppressions() GDPR erasure and its audit
events() stats() read your own data
graphSharing() opt into the cross-operator graph
health() public liveness and intel dataset sizes

Runnable versions in examples/.

Errors

Everything throws Kaidn\KaidnException, carrying the API's own message.

use Kaidn\KaidnException;

try {
    $r = $client->score(['event' => 'signup', 'email' => $email]);
} catch (KaidnException $e) {
    if ($e->getStatus() === 429) {
        notifyOps('Kaidn quota exhausted');
    }
    throw $e;
}

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

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]);
} catch (KaidnException $e) {
    $r = null;   // 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 release 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 key in the array you pass to score() is sent untouched.

Configuration

new Client($apiKey, [
    'base_url' => 'https://api.kaidn.io',
    'timeout'  => 10,                      // seconds per attempt
    'retries'  => 2,                       // extra attempts on a transient failure
    'cookie'   => new CookieOptions(),     // omit to keep cookie handling off
]);

Links

MIT