API reference
One GET per subject, one header for auth. Every check returns a decision, the signals behind it, and the evidence behind each signal.
Endpoints
| Endpoint | Auth | Purpose |
|---|---|---|
GET /health | no | Liveness. Returns { status: "ok" } and nothing else. |
GET /ready | no | Readiness: database reachable, index populated, and whether the feeds are stale. |
GET /v1/check/ip/{ip} | yes | IP reputation. IPv4 and IPv6. |
GET /v1/check/domain/{domain} | yes | Domain blocklists, plus the reputation of the IPs it resolves to. |
GET /v1/check/email/{email} | yes | Syntax, domain lists, MX, mail server reputation, role accounts, typos. |
GET /v1/signals | yes | The signal catalogue: every id this instance can emit, with meaning and guidance. |
GET /v1/profiles | yes | The policy profiles this instance offers, with thresholds and overrides. |
Authentication
Send
X-API-Key on everything under /v1/*. On a self-hosted instance the keys come from BOOTSTRAP_API_KEYS; they are stored as salted hashes, never in plain text.Choosing a policy
Add ?profile=<name> to any check. Same signals, different thresholds and weights — a checkout should be stricter than a newsletter signup. An unknown name returns 400 with the list of valid ones, and the signal reference shows what each profile on this instance does.
bash
# same address, three answers
curl -H 'X-API-Key: <key>' '<ENGINE>/v1/check/email/kunde@gmail.com?profile=newsletter' # allow
curl -H 'X-API-Key: <key>' '<ENGINE>/v1/check/email/kunde@gmail.com?profile=lead' # challenge
curl -H 'X-API-Key: <key>' '<ENGINE>/v1/check/email/x@mailinator.com?profile=lead' # blockResponse
json
{
"request_id": "4b1f…",
"ts": 1758045123,
"profile": "checkout",
"decision": "challenge",
"action": {
"type": "soft_block",
"retry_after_seconds": 300,
"reason": "score_in_challenge_band"
},
"score": 40,
"confidence": 0.7,
"signals": [
{
"id": "feed:tor_exits:0",
"category": "anonymizer",
"weight": 40,
"match": "185.220.101.1",
"source": "tor_exits",
"severity": "high"
}
],
"evidence": [
{
"source": "tor_exits",
"category": "anonymizer",
"match": "185.220.101.1",
"weight": 40,
"note": null
}
],
"enrichment": {
"ip": "185.220.101.1",
"country": "Germany",
"country_iso": "DE",
"asn": 205100,
"asn_organization": "Example AS",
"hostname": "tor-exit.example"
},
"timing_ms": {
"total": 12
}
}| Field | Meaning |
|---|---|
decision | allow | challenge | block — the answer you act on |
score | Sum of signal weights under the active profile, capped at 100 |
confidence | 0.2 with no signals, then 0.5 / 0.7 / 0.9 as total weight passes 0 / 30 / 80. How much evidence there is — not how likely abuse is |
signals | One entry per finding, with the weight it contributed |
evidence | The same findings keyed by source, for showing a human why |
enrichment | Facts gathered along the way: resolved IPs, MX hosts, deliverability, did_you_mean, geo/ASN |
action | Present on challenge only: retry_after_seconds and a machine-readable reason |
profile | The policy profile that produced this decision |
request_id | Correlates with the engine log line for this request |
No signals is not the same as clean
An empty
signals array means no source had anything to say about this address. That is an absence of evidence, and the confidence field says so.Errors
| Status | Meaning | Cause |
|---|---|---|
401 | Missing API key | No X-API-Key header was sent. |
403 | Invalid API key | The key is not known to this instance. |
400 | unknown_profile | The profile does not exist here. The response body lists the ones that do. |
429 | Rate limited | 60 requests per minute by default, 10 for the email check. Configurable per instance. |
Treat an error as “no answer”, not as “block”. The client packages do this for you.
curl
bash
curl <ENGINE>/health
curl <ENGINE>/ready
curl -H 'X-API-Key: <key>' <ENGINE>/v1/check/ip/185.220.101.1
curl -H 'X-API-Key: <key>' <ENGINE>/v1/check/domain/example.com
curl -H 'X-API-Key: <key>' <ENGINE>/v1/check/email/name@example.com
curl -H 'X-API-Key: <key>' <ENGINE>/v1/profiles
curl -H 'X-API-Key: <key>' '<ENGINE>/v1/signals?signal=email:no_mx'