API Reference
Integrate invisible bot & proxy detection into your product. No CAPTCHAs. No friction. Under 40ms per evaluation.
Last updated July 2026 · every API change lands on the changelog · additive-only stability policy
sk_test_sandbox key — no account required.Attack-specific playbooks: account takeover, card testing, bonus abuse, and more →
Quick Start
From zero to working integration in 5 minutes. You need your API key from the dashboard.
Using an AI coding assistant? Paste this prompt and it wires Sentinel into your app end-to-end — frontend script, backend check, env var, and a test. It follows the machine-readable guide at sntlhq.com/integrate.md.
Fetch https://sntlhq.com/integrate.md and follow it to add Sentinel fraud protection to this app — protect signup, login, and checkout. My API key is sk_live_YOUR_API_KEY; put it in a SENTINEL_KEY env var, never in client-side code. Then show me how to test it.
Loading…
// ——— STEP 1: Add SDK to your HTML <head> ————————————————————————————————————— <script async src="loading..."></script> <form class="monocle-enriched" id="login-form"> <input type="email" name="email" /> <!-- SDK auto-injects BOTH: name="monocle" (network) + name="sentinel_fp" (device) --> </form> // ——— STEP 2: Read both tokens in your frontend JS —————————————————————————————— document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); // Sentinel.collect() waits for the device layer, then returns both. const { token, fingerprintEventId } = await window.Sentinel.collect(); // Send to YOUR backend (never call Sentinel directly from the browser) await fetch('your-login-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, token, fingerprintEventId }) }); }); // ——— STEP 3: Evaluate on your backend (Node.js) —————————————————————————————— app.post('/your-login-endpoint', async (req, res) => { const { email, token, fingerprintEventId } = req.body; const result = await fetch('LOADING/v1/evaluate', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }, // Both layers — network + device — in one call. body: JSON.stringify({ token, fingerprintEventId }) }); const data = await result.json(); if (data.decision === 'block') { return res.status(403).json({ error: 'Bot or proxy detected.' }); } if (data.decision === 'review') { // e.g. require email verification or step-up auth } // ✓ Connection is clean — proceed res.json({ success: true }); });
Try It Live
Hit the sandbox endpoint with one click. No signup, no API key — just real /v1/evaluate response shapes you can copy-paste into your code.
Production calls use POST /v1/evaluate with a token from the SDK and an Authorization: Bearer sk_live_... header. The sample endpoint returns the same response shape so you can wire your parsing logic before signing up.
How It Works
Sentinel uses a 3-step Client → Your Backend → Sentinel API flow to keep your secret key off the browser.
The Sentinel SDK runs invisibly in your user's browser, collecting telemetry. It injects an encrypted token into your forms.
Your frontend submits the token to your own backend server along with the rest of the form data.
Your backend calls POST /v1/evaluate with your secret key. Sentinel returns a threat intelligence report instantly.
Authentication
All requests to /v1/evaluate must include your secret API key as a Bearer token. Find your key in the Dashboard.
sk_live_… in your HTML, JavaScript, or any client-side code. Only use it in your backend server environment.Embed the SDK
Add the Sentinel SDK to every page where you want to evaluate users. One script loads both detection layers — network intelligence (VPN, proxy, datacenter) and device intelligence (antidetect browsers, bots, tampering) — and injects both tokens into your forms. The key is already configured for your account.
<!-- Sentinel SDK — network + device, paste inside <head> --> <script async src="loading..."></script> <!-- Add class="monocle-enriched" to any form you want evaluated --> <form class="monocle-enriched" id="my-form"> <!-- The SDK injects both automatically: --> <input type="hidden" name="monocle" value="eyJ..." /> // network <input type="hidden" name="sentinel_fp" value="a1b2..." /> // device </form>
The SDK takes ~1–2 seconds to load and generate the tokens. If the device layer can’t run (e.g. a hardened browser blocks it), evaluation continues on the network layer alone.
Collect the Tokens
On submit, call Sentinel.collect() — it waits for the device layer to settle and returns { token, fingerprintEventId }. Forward both to your backend. (Or read the monocle and sentinel_fp hidden inputs directly.)
document.getElementById('my-form').addEventListener('submit', async (e) => { e.preventDefault(); // Both layers: network token + device event id const { token, fingerprintEventId } = await window.Sentinel.collect(); if (!token) { // Network SDK still loading — ask user to try again return showError('Security check loading. Please try again.'); } // Forward both to your backend with the rest of the form data const res = await fetch('/your-backend-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: e.target.email.value, token, fingerprintEventId // ← both layers to your backend }) }); });
Call the Evaluate Endpoint
loading…
Call this from your backend server — never from the browser. Pass the token from the client and your secret API key.
Request Body
input[name="monocle"] field.sentinel_fp; forward it here (either name is accepted) to unlock the device.* signals (antidetect, automation, emulator, …). Omit it and you get a network-only verdict.fingerprintEventId, Sentinel counts how many distinct accounts this device has been linked to under your API key and returns device.linked_accounts / device.multi_account — multi-accounting detection with zero extra integration. Stored as a one-way hash only.email.disposable to the response — checked against a continuously refreshed feed of thousands of burner/disposable domains. A disposable hit adds the disposable_email reason, raises risk_score, and escalates allow to review. The address is checked transiently and never stored or logged.Testing without a browser: send a deterministic test token — test_clean, test_vpn, test_proxy, test_datacenter, or test_tor — to exercise your allow / review / block handling end-to-end. Test calls are authenticated and rate-limited like real ones but never billed, stored, or webhooked, and the response carries "test": true.
CI & staging without an account: the public sandbox key sk_test_sandbox accepts the same test_* tokens and returns the same deterministic shapes — no signup, nothing billed, nothing stored, never touches a real quota. It only answers test tokens (live traffic still needs your real key) and is rate-limited per IP. curl -X POST https://sntlhq.com/v1/evaluate -H "Authorization: Bearer sk_test_sandbox" -H "Content-Type: application/json" -d '{"token":"test_vpn"}'
Your own test key (full pipeline, zero footprint): every account also has a personal sk_test_… key (Settings → API Key), and unlike the sandbox it runs the complete live pipeline — real tokens, real device intelligence, your rules and exception pins included. Events it creates are badged as test in the console, excluded from usage and stats, never fire webhooks or limit emails, and the response carries "test": true. It has its own hourly bucket and is deliberately exempt from the IP allowlist, so CI and laptops can exercise the pipeline without punching holes in your production restriction.
const response = await fetch('LOADING/v1/evaluate', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ token: req.body.sentinelToken // from the form submission }) }); const data = await response.json(); // data.decision → 'allow' | 'review' | 'block' (covers VPN, proxy, // datacenter, Tor, antidetect, automation) — route on this // data.reasons → machine-readable why if (data.decision === 'block') { return res.status(403).json({ error: 'Suspicious connection.' }); }
import requests response = requests.post( 'LOADING/v1/evaluate', headers={ 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'token': sentinel_token # from the form POST body } ) data = response.json() if data['decision'] == 'block': return '403 Suspicious connection', 403
curl -X POST 'LOADING/v1/evaluate' \ -H 'Authorization: Bearer sk_live_YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"token": "eyJ..."}'
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => 'LOADING/v1/evaluate', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer sk_live_YOUR_API_KEY', 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode([ 'token' => $_POST['sentinelToken'] ]) ]); $data = json_decode(curl_exec($ch), true); if ($data['decision'] === 'block') { http_response_code(403); exit(); }
Response Object
A successful call returns 200 OK with this JSON structure.
| Field | Type | Description |
|---|---|---|
| decision | string | Recommended action: "allow", "review", or "block". Advisory only — you set the policy. The reasons array explains every decision so you can log it or show an adverse-action reason to your own end user. If you configure custom rules in your dashboard, this field returns your action for a matching signal (most-severe rule wins). |
| engine_decision | string | Present only when your own policy changed decision: the engine's own risk-score verdict, before your policy was applied. Alongside it, decision_source is "rules" (with rule_matched listing the signals that triggered a rule) or "exception" (with exception_matched naming the per-IP/visitor pin from your dashboard's Exceptions list — explicit pins outrank signal rules). |
| risk_score | integer | Composite risk score 0–100 across all network and device signals. Not altered by custom rules. |
| isSuspicious | boolean | Legacy convenience flag. true when VPN, proxy, antidetect, automation, or emulator fired. Tor and datacenter signals raise risk_score and drive decision (Tor blocks) but do not set this flag — route on decision for full coverage. |
| ip | string | The unmasked, real IP address of the user. Mirrored at details.ip. |
| country | string | ISO 3166-1 country code (e.g. "US", "EE"). Mirrored at details.cc. |
| network.vpn | boolean | Commercial VPN detected (NordVPN, Proton, ExpressVPN, etc.). Mirrored at details.vpn. |
| network.proxy | boolean | Residential or SOCKS/HTTP proxy detected. Mirrored at details.proxied. |
| network.datacenter | boolean | Traffic from a known datacenter / cloud provider ASN. Mirrored at details.dch. |
| network.tor | boolean | Tor exit node detected. Mirrored at details.tor. |
| network.anonymous | boolean | Any anonymization layer detected. Mirrored at details.anon. |
| network.residential | boolean | true when neither datacenter nor proxy fired (looks like a real home IP). |
| network.service | string | Identified provider when known (e.g. "PROTON_VPN", "BRIGHT_DATA"). Mirrored at details.service. |
| device.antidetect | boolean | Antidetect / fingerprint-spoofing browser (Multilogin, Kameleo, GoLogin, etc.). Only present when fingerprintEventId was sent. |
| device.automation | boolean | Automated browser detected (Puppeteer, Playwright, Selenium). |
| device.emulator | boolean | Mobile emulator. |
| device.virtual_machine | boolean | Virtual machine. |
| device.incognito | boolean | Private / incognito browsing mode detected. |
| device.privacy_mode | boolean | Enhanced privacy settings active (e.g. Brave Shields, Firefox resist-fingerprinting). |
| device.ip_blocklisted | boolean | The visitor IP appears on email-spam or attack-source blocklists. |
| device.high_activity | boolean | This device is being identified unusually often (High-Activity Device). Informational — frequent visits alone aren't fraud, but combined with other signals they often indicate automation or farming. Adds reason code high_activity_device. |
| device.visitor_id | string | Stable device fingerprint hash. Same device returns the same id across sessions even after cookies clear — useful for ATO defense and device clustering. |
| device.tampering_score | number | 0–1 tampering score. Above 0.6 strongly suggests an antidetect browser; 0.3–0.6 means soft inconsistencies; below 0.3 is normal. |
| device.times_seen | integer | How many times Sentinel has seen this device in the last 90 days, keyed on a one-way hash of the visitor id (the raw id is never stored). 1 means a brand-new device — new devices on high-value actions are a classic fraud tell. |
| device.returning | boolean | True when times_seen > 1 — this device has been evaluated before. |
| device.linked_accounts | integer | How many distinct accounts this device has been seen with under your API key in the last 90 days. Only present when you pass your own accountId in the request alongside fingerprintEventId. Both identifiers are stored as one-way hashes only. |
| device.multi_account | boolean | True when linked_accounts > 1 — the core multi-accounting signal (bonus abuse, trial farming, duplicate signups). Adds reason code multi_account_device. |
| reasons | string[] | Machine-readable codes for which signals fired (e.g. "vpn_detected", "datacenter_asn", "antidetect_browser"). |
| evaluated_in_ms | integer | Server-side processing time for this request, in milliseconds. |
{
"status": "success",
"isSuspicious": true,
"decision": "review",
"risk_score": 71,
"ip": "185.107.80.12",
"country": "EE",
"network": {
"vpn": true,
"proxy": false,
"datacenter": true,
"tor": false,
"anonymous": true,
"residential": false,
"service": "PROTON_VPN"
},
"reasons": ["vpn_detected", "datacenter_asn"],
"evaluated_in_ms": 28,
"details": { /* legacy mirror */ "ip": "185.107.80.12", "cc": "EE", "vpn": true, "proxied": false, "anon": true, "dch": true, "service": "PROTON_VPN" }
}{
"status": "success",
"isSuspicious": false,
"decision": "allow",
"risk_score": 0,
"ip": "82.131.45.9",
"country": "DE",
"network": {
"vpn": false,
"proxy": false,
"datacenter": false,
"tor": false,
"residential": true,
"service": "DEUTSCHE_TELEKOM"
},
"reasons": [],
"evaluated_in_ms": 22
}IP Lookup
A verdict for a bare IP address — no browser token needed. Same key, same hourly bucket as /v1/evaluate.
GET /v1/lookup/{ip} — for server-side screening where no client runs: allowlist checks, batch scoring, enriching your own logs. Network signals only (there is no device to fingerprint), so use /v1/evaluate whenever a browser is involved.
curl https://sntlhq.com/v1/lookup/185.220.101.34 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
{
"ip": "185.220.101.34",
"known": true,
"verdict": "block", // allow | review | block
"risk_score": 90,
"signals": { "vpn": false, "proxied": false, "tor": true, "dch": false, "anon": true },
"network": { "asn": "AS205100", "org": "F3 Netze", "country": "DE", "city": null },
"latency_ms": 4
}
known: false means no intelligence source had an opinion — the verdict is then allow with risk_score: 0, which is “nothing found,” not a clean guarantee. Exception pins you configure in the dashboard apply here too (verdict_source: "exception" with engine_verdict preserved, additively). Signal spellings (proxied, dch) are frozen — safe to parse.
Rate Limits
Sentinel is in open beta — all access is free. Two limits run in parallel; whichever you hit first returns 429.
| Scope | Limit |
|---|---|
| Per API key (Free) | 1,000 requests / hour |
| Per API key (Growth) | 5,000 requests / hour |
| Per source IP | 50,000 requests / hour |
| Enterprise | Custom — contact us |
No monthly cap during beta. The per-IP ceiling is a backstop against runaway abuse from a single machine. Retry-After is included on every 429 response.
Every keyed response also carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds) so you can back off before the 429 — and an X-Request-Id header you can quote to support when something looks wrong.
Reliability & fail-open behaviour
Sentinel fails open: if an upstream network- or device-intelligence provider is unreachable, the evaluation returns its best available signals rather than erroring, and a missing provider means those specific signals are simply absent (never a false "clean" guarantee). This keeps your checkout or login flowing during a provider blip instead of blocking everyone. Because of this, treat decision as advisory and set your own thresholds — for high-stakes actions, prefer step-up verification on "review" over hard-allowing, so a degraded evaluation can’t silently wave through a threat.
Error Codes
All error responses include an error string field.
-
400
Bad Request — Missing or invalid
token. - 401 Unauthorized — Invalid or missing API key.
-
403
Forbidden — Account suspended (contact [email protected]), or the caller's IP is not on your key's IP allowlist (Settings → API Key Security; the
hintfield names the calling address). Not retryable from the same address. - 429 Rate Limited — Hourly request limit exceeded (1,000/hr per key on Free, 5,000/hr on Growth, 50,000/hr per IP).
- 500 Server Error — Unexpected internal error. Safe to retry with backoff.
Webhooks
Threat alerts pushed to Slack, Discord, or your own endpoint — configured from the dashboard Tools drawer.
When they fire. On engine-detected threats (VPN, proxy, Tor, datacenter with tampering, bots, antidetect browsers) and on any evaluation your own custom rules decided to block. Slack and Discord URLs get a channel-ready message; any other HTTPS endpoint receives a JSON event (event: "threat.detected" with IP, threat type, and the signal details).
Verifying authenticity. Every delivery is signed with your account's signing secret (shown next to the webhook URL in the dashboard). Compute HMAC_SHA256(secret, timestamp + "." + rawBody) using the X-Sentinel-Timestamp header and compare it (constant-time) to the hex digest in X-Sentinel-Signature (after the sha256= prefix). Reject anything unsigned, mismatched, or older than 5 minutes.
Delivery health. The last delivery outcome is shown in the dashboard header chip; if 5 consecutive deliveries fail we email you — a dead webhook must never read as “no threats.” Deliveries time out after 10 s and never follow redirects.
Generic-endpoint payload. What your receiver actually gets (Slack/Discord URLs get a formatted message instead):
{
"event": "threat.detected",
"ip": "185.220.101.34",
"threat_type": "Tor", // or "Custom rule (…)" / "Customer exception (…)"
"details": {
"ip": "185.220.101.34", "cc": "DE",
"vpn": false, "proxied": false, "tor": true, "dch": false,
"bot": false, "tampering": false, "antidetect": false,
"decision": "block",
"rule_matched": null, // signals, when your rule caused the block
"exception_matched": null // pins, when your exception caused it
}
}
MCP for AI Agents
A hosted Model Context Protocol server — point an AI agent at it and it can screen IPs with real Sentinel verdicts.
Endpoint: POST https://sntlhq.com/mcp (streamable HTTP, stateless). Tools: lookup_ip — the same verdict as GET /v1/lookup — and service_status. Anonymous use shares the free tool's tight per-IP limits; add Authorization: Bearer sk_live_… for 1,000 lookups/hour on your own quota (your IP allowlist applies here too).
{
"mcpServers": {
"sentinel": {
"type": "http",
"url": "https://sntlhq.com/mcp",
"headers": { "Authorization": "Bearer sk_live_YOUR_API_KEY" }
}
}
}
API Stability & Versioning
What you can build on without worrying about the ground moving.
Versioning. The API is versioned in the path (/v1/). Within a major version we make only additive changes: new response fields, new optional request parameters, new signal reasons. Your integration must tolerate unknown fields in responses — that is the only forward-compatibility requirement we place on you.
Breaking changes. Renaming or removing response fields, changing types or semantics, or retiring an endpoint only happens in a new major version (/v2/). When that day comes, /v1/ keeps working for at least 12 months after the announcement.
Deprecation notice. Any deprecation is announced at least 90 days in advance via the changelog and email to affected API keys, with a documented migration path. Enterprise agreements can pin longer support windows — [email protected].