Server-side verification
The browser SDK provides signals, but browser payloads can be spoofed — use server-side verification for enforcement decisions. Secret API keys never belong in client-side code.
Endpoint
Secret-key authenticated
Authorization: Bearer sec_xxxsec_xxx is the site's secret key from the Shield dashboard. Call the endpoint from your server only. A public site key (pub_...) is rejected.
The endpoint is also described as an OpenAPI document.
// Request
{
"session_id": "sess_xxx",
"event": "signup_attempt"
}
// Response
{
"verified": true,
"risk_status": "available",
"risk_score": 87,
"recommendation": "challenge",
"challenge_passed": false,
"challenge_passed_at": null,
"session": {
"platform": "web",
"visitor_id": "vis_xxx",
"first_seen_at": "2026-09-08T10:00:00.000Z",
"last_seen_at": "2026-09-08T10:05:00.000Z",
"event_count": 4,
"pageview_count": 2,
"risk_reasons": ["vpn_detected", "payment_attempt"],
"ip_rotating": true,
"asn_rotating": false,
"country_rotating": false,
"distinct_ips": 2,
"distinct_asns": 1,
"distinct_countries": 1,
"ips": ["203.0.113.7", "203.0.113.9"],
"countries": ["IL"],
"last_ip": "203.0.113.9",
"last_country": "IL",
"last_country_name": "Israel",
"last_asn": 1680,
"last_asn_name": "Partner",
"last_organization": "Partner Communications",
"network": {
"vpn": true, "proxy": false, "tor": false, "relay": false,
"hosting": false, "datacenter": false, "malicious": false
}
}
}session is the picture Shield has of the whole session, for your own records — an order notification, a fraud queue, a CRM note. risk_reasons are the distinct reasons across every event; ip_rotating, asn_rotating and country_rotating are true when the session was seen from more than one IP, network or country; network flags are true if any event of the session came from that kind of address; last_* describe the most recent event. ips holds at most 10 addresses. platform says where the most recent event came from: web for a website, webview for a page inside a mobile app, ios or android for a native app (see Mobile apps). Identity is not included — your backend already knows who its customer is.
challenge_passed is true once the visitor completed a Cloudflare Turnstile challenge that Shield verified for this session (see In-page enforcement). Accept challenge only when it is true and reject block to turn in-page friction into a decision no client can bypass. risk_status is unknown when no event of the session had IP intelligence; treat it as monitor, never as safe.
Recommended flow
signup_attempt and receives the initial risk signal.session_id./v1/shield/sessions/verify with your secret API key.verified is false or the call fails.Request and response fields
Request fields
| Field | Required | Description |
|---|---|---|
session_id | yes | The Shield session ID your backend received from the browser. A string; only the first 128 characters are used. |
event | no | The step you are about to allow, for example signup_attempt. The endpoint accepts this field and currently ignores it: any value, or none, returns the same response. No list of values is enforced. |
The body is parsed as JSON whatever Content-Type you send. Other fields are ignored.
Response fields
| Field | Type | Description |
|---|---|---|
verified | boolean | true: Shield has this session for your site, and the fields below describe it. false: it does not, see When verified is false. |
risk_status | available or unknown | available: at least one event of the session was scored. unknown: none had IP intelligence, so there is no score. |
risk_score | integer 0 to 100, or null | The highest risk score of any event in the session. null when risk_status is unknown. |
recommendation | allow, monitor, challenge or block | The recommendation Shield gave for the session's most recent event. When none is stored: allow if the session has a score, monitor if it has none. It is a suggestion for your code: Shield does not act on it here. |
challenge_passed | boolean | The visitor completed a Turnstile challenge that Shield verified for this session. |
challenge_passed_at | timestamp or null | When that happened. |
session | object | The session picture described under the response example above. |
A score and its reasons are risk signals, not proof of abuse. Corporate VPNs, mobile carriers and privacy tools all produce them for ordinary customers.
When verified is false
Unknown to Shield: not safe, and not risky
{
"verified": false,
"risk_score": null,
"recommendation": null
}The request succeeded (200), and Shield has no session with that ID for the site that owns the secret key. Exactly these three fields are returned: there is no risk_status, challenge_passed or session.
It means "unknown to Shield", not "safe" and not "risky". Usual causes:
- The SDK did not run in that browser: blocked by an extension or a network filter, JavaScript off, or the request did not come from a browser at all.
- The ID was made up or altered, or the field reached your backend empty or cut short.
- The ID belongs to another site (another site key).
- Your backend verified before the browser's first event arrived. This is rare with
autoTrackon, because the SDK sends a page view as soon as it starts.
Decide in your own code what an unverifiable session may do. For a sensitive step such as creating an account, a common choice is to treat it like challenge: ask for an extra check rather than letting it straight through. Do not read it as allow.
Errors
Errors are JSON with one field, { "error": "<code>" }.
| Status | error | When |
|---|---|---|
400 | invalid_json | The body is not valid JSON. |
400 | session_id_required | session_id is missing, empty or not a string. |
401 | invalid_secret_key | The Authorization header is missing, is not Bearer followed by a key that starts with sec_, or the key belongs to no site. |
429 | rate_limited | Too many verify requests with this secret key in the current minute. The default limit is 1,000 per minute per key, counted separately from the browser SDK's events. No Retry-After header is sent; retry in the next minute. |
500 | internal_error | An unexpected failure on Shield's side. A body that is valid JSON but not an object, such as null, also returns this. |
The checks run in this order: the header format (401), the rate limit (429), the key lookup (401), then the body (400). A session Shield does not know is never an error; it is the verified: false response.
If the call fails or times out, your backend has no verdict. Treat that as "risk unknown" and apply your own default; do not treat it as allow.
Plain HTML forms
SDK 1.11.0 and later
For a form the browser posts itself, let the SDK add the session ID as a hidden field:
FindIP.init({ siteKey: 'pub_xxxxxxxxx', sessionField: true });Every form that POSTs to your own origin then carries findip_session=<session id>; read it on the server like any other form field. Pass a string to choose another name. This also works for visitors whose browser blocks cookies, where reading the _fip_sid cookie on the server finds nothing.
Integration example
// Frontend: include session ID in the signup request
const { sessionId } = FindIP.getSession();
await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify({ email, password, findip_session_id: sessionId }),
});
// Backend: verify before creating the account
const verify = await fetch('https://shield.findip.net/v1/shield/sessions/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FINDIP_SHIELD_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_id: req.body.findip_session_id,
event: 'signup_attempt',
}),
});