loader

Sentinel API Reference

Query IP reputation, verify human-proof tokens, and embed the captcha widget. Everything you need to integrate Sentinel.

Authentication

Authenticated API endpoints live under /api/v1 and are authenticated with an API key sent in the X-Api-Key request header. Every request is scoped to the key's owner, plan and quota.

Header

X-Api-Key: rk_xxxxxxxx_your_secret
The captcha does not need an API key. To verify a captcha, your server posts your site's Secret Key to /captcha/siteverify — no X-Api-Key. The API key here is only for the IP‑reputation & AI endpoints below. Manage your Sentinel sites (Site Key + Secret Key) in the Lab.

Identity & quota

GET /api/v1/me

Returns the calling API key's identity, plan, remaining credits and quota. Useful for confirming a key works and checking limits before a batch of calls.

Example request

curl https://redeyed.com/api/v1/me \
  -H "X-Api-Key: rk_xxxxxxxx_your_secret"

Example response

{
  "key": {
    "name": "Production",
    "prefix": "xxxxxxxx",
    "scopes": ["ip:read", "verify"],
    "last_used_at": "2026-06-22T01:14:09Z"
  },
  "plan": "team",
  "credit_balance": 9820,
  "quota": 48000
}

IP reputation

GET /api/v1/ip/{ip?}

Scores an IP address. Omit {ip} to score the caller's own IP. Returns a 0–100 score, a risk band, flags (proxy / VPN / Tor / datacenter / bot / abuser), geolocation and ASN. Each lookup is recorded as a behavioral signal for the reputation engine.

Path parameters

ParamTypeDescription
ipstring (optional)IPv4 or IPv6 address. If omitted, the caller's IP is scored. Invalid input returns 422.

Example request

curl https://redeyed.com/api/v1/ip/8.8.8.8 \
  -H "X-Api-Key: rk_xxxxxxxx_your_secret"

Example response

{
  "ip": "8.8.8.8",
  "score": 72,
  "risk": "medium",
  "flags": {
    "is_proxy": false,
    "is_vpn": false,
    "is_tor": false,
    "is_datacenter": true,
    "is_residential_proxy": false,
    "is_bot": false,
    "is_abuser": false
  },
  "connection_type": "hosting",
  "network_kind": "hosting",
  "country_code": "US",
  "asn": 15169,
  "asn_org": "Google LLC",
  "signals": { /* score breakdown */ },
  "evidence": { "datacenter": "confirmed" },
  "confidence": 61,
  "last_seen_at": "2026-06-22T01:10:00Z",
  "usage": { /* request quota usage */ }
}

Verify a captcha

POST /captcha/siteverify

Server-to-server human verification — reCAPTCHA/Turnstile style. From your backend, post your site's Secret Key together with the response token your form collected from the widget. The secret alone identifies and authorizes the site, so no developer API key is required. Returns the outcome band, score and reason.

Get your Site Key (public) and Secret Key (private) from Sentinel → Sites. The Secret Key is shown once when you create the site. Keep it server-side — never put it in page markup.

Body parameters

ParamTypeDescription
secretstring (required)Your site's Secret Key (st_sec_…). Server-side only.
responsestring (required)The captcha-token minted by the widget on a successful solve. (token is accepted as an alias.)
remoteipstring (optional)The end user's IP, if your server knows it. Improves scoring.
telemetryobject (optional)Coarse, non-PII behavioral telemetry to feed bot scoring.

Example request

curl -X POST https://redeyed.com/captcha/siteverify \
  -H "Content-Type: application/json" \
  -d '{
    "secret": "st_sec_xxxxxxxxxxxxxxxxxxxx",
    "response": "captcha-token-from-widget",
    "remoteip": "203.0.113.42"
  }'

Example response

{
  "success": true,
  "outcome": "passed",   // passed | challenged | blocked
  "score": 18,
  "reason": "Acceptable risk (score 18)",
  "hostname": "yoursite.com",
  "error-codes": []
}

Outcomes: passed — allow the request; challenged — re-prompt or step up; blocked — deny (high-risk IP or detected automation).

Alternative: the developer API

Already using the /api/v1 key-based API? You can also verify with POST /api/v1/verify (X-Api-Key + site_key + token). It returns the same outcome. For a plain captcha integration, prefer /captcha/siteverify above.

Widget embed

The Sentinel widget is a self-contained, vanilla-JS captcha — no build step. Load the script once, then drop a .redeyed-captcha element with your site key inside any form. The widget auto-mounts, runs the challenge, and injects a hidden captcha-token input on success.

Embed

<!-- Load once, anywhere on the page -->
<script src="https://redeyed.com/captcha.js" async></script>

<!-- Place inside the form you want to protect -->
<form method="POST" action="/signup">
  <!-- ...your fields... -->
  <div class="redeyed-captcha" data-sitekey="site_xxxxxxxxxxxx" data-width="full"></div>
  <button type="submit">Create account</button>
</form>

On success the widget appends <input type="hidden" name="captcha-token"> to the enclosing form and fires a captcha:solved event. Submit the token to your backend and confirm it with POST /captcha/siteverify (your Secret Key + the token). You can also drive the widget programmatically with window.Captcha.render() and window.Captcha.reset().

Customize per form

The same site key works everywhere — add optional data-* attributes to each .redeyed-captcha element to tune the widget per form. All are optional.

AttributeValuesDescription
data-sitekeyrequiredYour public Sentinel site key.
data-widgetsee challenge typesChallenge style. Defaults to adaptive, which escalates from a low-friction proof to procedural reasoning based on risk.
data-themeauto · light · darkColour theme. auto follows the visitor's prefers-color-scheme and re-renders live if they change it.
data-schemesee colour schemesColour scheme — surface & accent tinted to match your form.
data-widget-steps1-7Paid plans. Pin how many verification steps the widget asks for, on any widget type. Like data-difficulty it is a floor: it can raise the number of steps above the adaptive baseline, never lower it, so a risky visitor is never given an easier ride than they would otherwise get. Ignored entirely on a free plan.
data-difficultyeasy · medium · hard · max · 1-6Challenge strength. Higher = a harder puzzle and more of them: longer holds, tighter drag and rotation targets, busier scenes, bigger image grids, more rounds, harder proof-of-work. Only raises difficulty above the adaptive baseline — a risky visitor is always challenged hard regardless.
data-widthe.g. 100% · 320px · fullWidget width. Use 100% or full for full-width responsive forms.
data-badgehideHides the Redeyed badge for this embed. Requires a Team or Enterprise plan — on any other plan the attribute is inert and the badge stays visible. The server decides, so it cannot be bypassed from the DOM. To hide it across every embed instead, use the account setting in Lab → Sentinel → Sites → Settings.

Procedural visual challenges include a keyboard-accessible verification switch. Visitors who prefer reduced motion are moved automatically to the non-animated press-and-hold proof.

<div class="redeyed-captcha"
     data-sitekey="site_xxxxxxxxxxxx"
     data-widget="press_hold"
     data-theme="dark"
     data-difficulty="hard"></div>

Attack mode

POST /captcha/attack-mode

Raise the challenge floor for every visitor to your site, immediately. Sentinel already arms this on its own when it sees a flood, but detection is reactive — it can only trip once traffic has been arriving for a counter window. Your WAF, CDN or on-call alerting usually notices first, so this endpoint lets that system raise the floor without a human opening a browser.

Authenticated with your site's Secret Key, like /captcha/siteverify — no X-Api-Key. The change is scoped to the site that key belongs to.

Sentinel Pro. Raising the floor on demand needs an active Sentinel Pro add-on (or a membership tier that bundles it). Automatic attack mode is free on every plan — Sentinel still raises this floor by itself when it detects a flood, whatever you pay. Pro is what lets you get ahead of one.

Request

FieldTypeDescription
secretstringYour site's Secret Key.
statestringelevated, under_attack, or normal to clear.
minutesint (optional)How long to hold it. 1–1440, default 30. Ignored for normal.

Example request

curl -X POST https://redeyed.com/captcha/attack-mode \
  -H "Content-Type: application/json" \
  -d '{"secret":"st_sec_xxxxxxxx","state":"under_attack","minutes":30}'

Example response

{
  "success":    true,
  "state":      "under_attack",
  "held":       true,
  "expires_in": 1800,
  "hostname":   "example.com",
  "applies": {
    "risk_band":        "medium",
    "difficulty_floor": 5,
    "rounds_floor":     3
  }
}

Clearing it

curl -X POST https://redeyed.com/captcha/attack-mode \
  -H "Content-Type: application/json" \
  -d '{"secret":"st_sec_xxxxxxxx","state":"normal"}'

A hold expires on its own, so a forgotten one cannot punish your site indefinitely.

StatusMeaning
200Applied. state reflects what is now in force.
401 invalid-input-secretUnknown secret, or the site is inactive.
403 sentinel-pro-requiredOn-demand attack mode needs Sentinel Pro. Automatic protection is unaffected.
422Invalid state or minutes.
503 hold-not-appliedThe change did not take. Treat as a failure and retry — see the note below.
503 attack-mode-disabledAttack mode is switched off server-side.
This endpoint does not fail open. Unlike the risk-assessment endpoints, it reports an error rather than pretending to succeed: an automation that believes it raised protection when it did not is worse than a visible failure. Always check success.
Raising the floor adds friction for every visitor, not just attackers. Prefer elevated unless you are actually under attack, and keep the hold short — you can always re-issue it.

Attack alerts

Sentinel raises your challenge floor by itself when it detects a flood — on every plan, including free. Alerts tell you the moment it happens, so you can look at your own logs while it is going on rather than reading about it in a support ticket the next morning.

Configure a webhook URL and/or an email under Sentinel → Sites → your site. Alerts need an active Sentinel Pro add-on; the protection they report on does not.

Payload

{
  "event":       "attack_mode.changed",
  "site":        "example.com",
  "from":        "normal",
  "to":          "under_attack",
  "direction":   "raised",
  "occurred_at": "2026-08-25T14:03:11+00:00",
  "applies": {
    "risk_band": "medium", "difficulty_floor": 5, "rounds_floor": 3
  }
}

applies is null when direction is cleared.

Verifying the signature

Every call carries X-Sentinel-Timestamp and X-Sentinel-Signature. The signature is an HMAC-SHA256 of timestamp + "." + rawBody, keyed with the signing secret shown once when you saved the webhook.

PHP

$raw  = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_SENTINEL_TIMESTAMP'] ?? '';
$sig  = $_SERVER['HTTP_X_SENTINEL_SIGNATURE'] ?? '';

$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);

// Constant-time compare, and reject anything older than five minutes so a
// captured call cannot be replayed at you later.
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
    http_response_code(400);
    exit;
}
One alert per change, not per request. A site under sustained attack is told once when the floor rises and once when it drops, however many requests arrive in between.

Network operators

If you run the address space we score — a VPN, a host, an ISP — you see the consequences of our verdicts without any view of the cause: one abuser on a shared exit degrades that address for every legitimate user behind it, and the first you hear of it is a support ticket saying "your service is broken".

These endpoints give you that view for your own networks, so the responsible account can be terminated and the address can recover. They require Sentinel Pro and an X-Api-Key.

Verified claims only. Everything below is scoped to networks you have claimed and we have verified. The same data across networks you do not operate is a targeting list, so there is no way to ask about an arbitrary ASN here. Claims are reviewed by a person — an ASN cannot be proven with a DNS record the way a domain can. Start at Sentinel → Sites or contact support with evidence you operate the network.
GET /api/v1/network/claims

Your claims and their status. verified_networks: 0 means nothing else here will return data yet.

POST /api/v1/network/bulk

Reputation for up to 500 addresses in one call, for operators holding thousands of exits. Billed per address resolved, the same as asking for each one individually — batching saves round trips, not money. Addresses you have not claimed are allowed here: it returns only what the per-IP endpoint already would.

curl -X POST https://redeyed.com/api/v1/network/bulk \
  -H "X-Api-Key: rk_xxxxxxxx_your_secret" \
  -H "Content-Type: application/json" \
  -d '{"ips":["203.0.113.4","198.51.100.7"]}'

An address we have never seen comes back "known": false with a null score, rather than being reported as clean. Absence of evidence is not evidence.

GET /api/v1/network/health

Per-ASN summary: how many addresses we know, the split by risk band, and the worst offenders first. The question you actually have when tickets start arriving.

GET /api/v1/network/abuse

Abuse observed on your verified networks. Takes since (ISO 8601, default 7 days) and limit (max 1000).

{
  "since": "2026-08-18T00:00:00+00:00",
  "count": 2,
  "events": [
    { "ip": "203.0.113.4", "observed_at": "…", "category": "automation", "severity": "high" }
  ]
}
What this deliberately does not tell you. The feed reports the offending address and the category of abuse. It never reports which site was targeted, what was submitted, or anything about the person behind the address. You need enough to find the account on your side and nothing more — and a feed that leaked end-user activity would be unusable by exactly the operators it is built for.

Capabilities

GET /captcha/capabilities

Returns every challenge type, colour scheme, theme and difficulty level this deployment accepts. No API key and no Site Key — the widget script already ships the same tables to the browser. Cached for 5 minutes.

Use it instead of hardcoding a list. An unrecognised data-widget is not an error — it falls back to the site default — so a stale hardcoded list keeps working while silently ignoring the setting. Plugin settings screens should build their dropdown from this endpoint.

Example request

curl https://redeyed.com/captcha/capabilities

Example response

{
  "widget_version": "20260824c",
  "types": {
    "meta":     ["adaptive", "all"],
    "concrete": ["behavioral", "pow", "press_hold", , "shape_match", "count_match"],
    "default":  "press_hold"
  },
  "themes":  ["light", "dark", "auto"],
  "schemes": [
    { "name": "default",  "premium": false },
    { "name": "midnight", "premium": true  }
  ],
  "difficulty": { "named": ["easy","medium","hard","max"], "min": 1, "max": 6, "raises_only": true }
}
A scheme marked "premium": true needs a paid plan. A free site that asks for one silently renders default, so show that in your UI rather than letting the choice appear to apply.

Challenge types

Set with data-widget. Two values are resolved server-side rather than naming a concrete challenge:

ValueBehaviour
adaptive Recommended. Resolves from IP reputation and the form's difficulty floor — clean traffic gets a low-friction proof, elevated risk escalates to procedural reasoning.
allPicks any supported type at random per challenge, so different visitors meet different styles.

Concrete types

ValueDescription
behavioral One click. Pointer entropy, timing and focus signals decide in the background — no puzzle at all.
pow Fully invisible. The browser solves a SHA-256 challenge; the hash is recomputed server-side, so the work cannot be faked.
text_math A short arithmetic question. No dragging or fine motor control.
image_puzzle Drag the missing piece into the gap. Drawn procedurally on canvas — no image assets to scrape or pre-solve.
rotate_align Turn the shape upright. Generated on canvas from the challenge seed, so every render is unique.
press_hold default Hold the button until it fills. Reads steadiness — trivial for a person, awkward for a script. Also the accessible fallback.
image_pick Choose the tile matching a prompt. Emoji rendered to noisy canvases, so there is no selectable text.
relational_scene Reason about distance and containment in a one-time scene instead of recognizing a reusable stock photo.
motion_track Follow moving objects through time and identify which reaches the marked destination.
light_shadow Infer a light source from the shadows it casts.
shape_match Spot the two 3D objects that match — same shape, or same colour. The scene, palette and question are regenerated every time, so nothing repeats.
count_match A key lists objects with a required quantity; select every matching object in the grid. The selected set is verified exactly, so selecting everything fails.
Every procedural visual challenge ships a keyboard-accessible switch to press_hold. Visitors with prefers-reduced-motion are moved off animated challenges automatically, so you never need a separate accessible embed.

Colour schemes

Set with data-scheme. Themed schemes tint the surface and compose with data-theme (light / dark / auto). Fixed schemes carry their own surface and ignore the theme. Animated schemes paint a moving backdrop and require a paid plan.

SchemeKindDescription
default Themed Brand red over our own light/dark surfaces.
ocean Themed Blue / teal.
forest Themed Green.
sunset Themed Orange.
graphite Themed Slate grey.
royalty Fixed Gold on near-black.
ruby Fixed Crimson on near-black.
hacker Fixed Terminal green on black, monospace. Adds a scanning band and grid on paid plans.
cyber Fixed Terminal cyan on black, monospace. Adds a scanning band and grid on paid plans.
monochrome Fixed Pure greyscale, no hue.
midnight Animated Night sky with a twinkling starfield and shooting star.
ember Animated Brand-red galaxy: aurora wash, starfield and shooting star.
aurora Animated Flowing aurora wash.
Premium schemes are enforced server-side. On a free plan a data-scheme="midnight" is inert and the widget falls back to default — it cannot be unlocked from the DOM.
<div class="redeyed-captcha"
     data-sitekey="site_xxxxxxxxxxxx"
     data-scheme="cyber"
     data-theme="dark"></div>

JavaScript API

Loading captcha.js defines window.Captcha. Any .redeyed-captcha[data-sitekey] element on the page mounts automatically on load — you only need these for widgets you add or reset dynamically.

MemberDescription
Captcha.render(target?)Mounts a widget. Pass an element or a CSS selector; omit to mount every unmounted widget on the page.
Captcha.reset(target?)Clears the token and issues a fresh challenge. Call this after a failed form submit so the visitor can retry.
Captcha.badge(target?)Re-mounts the badge on a widget.
Captcha.theme · Captcha.widget · Captcha.schemeThe resolved defaults for the current page, useful for debugging what the server actually applied.
window.CaptchaNoBadgeSet truthy before the script loads to request badge removal globally. Subject to the same plan gate as data-badge.

Events

On success the widget dispatches a bubbling captcha:solved CustomEvent whose detail.token is the single-use token, and writes that token into a hidden captcha-token input in the enclosing form.

document.addEventListener('captcha:solved', function (e) {
  // Send e.detail.token to your server and verify it there.
  console.log(e.detail.token);
});

// Re-arm after a rejected submit
window.Captcha.reset('#signup .redeyed-captcha');

Drop-in replacement

Migrating from reCAPTCHA, hCaptcha or Turnstile needs no markup changes. Swap the provider's script for captcha.js and keep your existing container — Sentinel adopts it and writes the response field your server already reads.

Existing containerResponse field written
.g-recaptchag-recaptcha-response
.h-captchah-captcha-response
.cf-turnstilecf-turnstile-response

The explicit JS APIs keep working too: grecaptcha, hcaptcha and turnstile shims are defined only when the real provider script is absent, so a live install is never clobbered. render(), reset(), ready() and getResponse() all behave as expected.

Server-side, these paths accept your existing verify payload and return the same response shape, so only the hostname changes:

POST https://redeyed.com/recaptcha/api/siteverify
POST https://redeyed.com/hcaptcha/siteverify
POST https://redeyed.com/turnstile/v0/siteverify
Send your Sentinel Secret Key as secret — your old provider's secret is not valid here. Everything else about the request and response is unchanged.

Challenge / solve flow

The widget uses two same-origin endpoints under the hood. You normally never call these directly, but they're documented for custom integrations.

1. Issue a challenge

POST /captcha/challenge

Issues an adaptive proof-of-work challenge for a site key. Difficulty scales with the IP's live risk band; high-risk IPs are blocked rather than challenged.

Request & response

curl -X POST https://redeyed.com/captcha/challenge \
  -H "Content-Type: application/json" \
  -d '{ "site_key": "site_xxxxxxxxxxxx" }'

// 200 OK
{
  "challenge_id": "aZ8...40-char-token",
  "nonce": "16-char-nonce",
  "difficulty": 3
}
// 403 { "error": "Access denied.", "blocked": true } for high-risk IPs

2. Solve the challenge

POST /captcha/solve

Find an integer solution so that sha256(nonce + solution) starts with difficulty leading hex zeros, then post it back. The server recomputes the hash, checks it's unsolved / unexpired / IP-matched, and mints a short-lived HMAC token.

Request & response

curl -X POST https://redeyed.com/captcha/solve \
  -H "Content-Type: application/json" \
  -d '{
    "challenge_id": "aZ8...40-char-token",
    "solution": "104729",
    "telemetry": { "solve_ms": 380, "pointer_entropy": 0.42 }
  }'

// 200 OK
{
  "token": "signed-captcha-token",
  "bot_likelihood": 7
}

Submit the returned token with your form, then confirm it server-side via POST /captcha/siteverify (your Secret Key + the token).

Reputation response fields

Fields returned by GET /api/v1/ip/{ip}.

FieldTypeDescription
ipstringThe IP address that was scored.
scoreintegerReputation score, 0–100. Higher means riskier.
riskstringRisk band derived from the score: clean, low, medium or high.
flags.is_proxybooleanIP is a known proxy.
flags.is_vpnbooleanIP belongs to a VPN provider.
flags.is_torbooleanIP is a Tor exit / relay node.
flags.is_datacenterbooleanIP is hosting / datacenter, not residential.
flags.is_residential_proxybooleanA consumer connection being used as a proxy exit node — the address is genuinely residential, but is seen across many unrelated sites, from many devices, at a rate a household does not produce. Inferred from behaviour, never from observing a proxy service, so evidence.residential_proxy is never confirmed. Treat it as a strong signal, not a fact.
flags.is_botbooleanIP is associated with automated / bot traffic.
flags.is_abuserbooleanIP appears on abuse / reputation blocklists.
country_codestring|nullISO 3166-1 alpha-2 country code (self-owned geo data).
asninteger|nullAutonomous System Number.
asn_orgstring|nullOrganization that owns the ASN.
connection_typestringe.g. residential, mobile, hosting, datacenter, education or unknown. Can be set by any classification we hold for the address.
network_kindstringWhat the network announcing this address is for, taken from the global routing table: residential, mobile, hosting, business, education, government or unknown. Separate from connection_type on purpose — when the two disagree, this is the one describing who the addresses belong to.
evidenceobjectHow well-evidenced each raised flag is, keyed by flag — confirmed (the range is published as such), inferred (a block dense with confirmed hosts), or assumed (the operator sells VPN or hosting capacity). Read this before blocking: an assumed match describes the network, not the address, and is scored at half weight for that reason.
confidenceintegerHow much history backs the score, rising with observed hits. Unrelated to evidence above.
last_seen_atdatetime|nullWhen this IP was last observed.
Ready to integrate? Create your API keys and Sentinel sites in the Lab.