Shield · Server verification

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

POST https://shield.findip.net/v1/shield/sessions/verify
Authorization: Bearer sec_xxx

sec_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

1. BrowserSDK tracks signup_attempt and receives the initial risk signal.
2. Your backendReceives the signup request including the session_id.
3. VerifyBackend calls /v1/shield/sessions/verify with your secret API key.
4. EnforceAllow, challenge, or block based on the verified response, and apply your own rule when verified is false or the call fails.

Request and response fields

Request fields

FieldRequiredDescription
session_idyesThe Shield session ID your backend received from the browser. A string; only the first 128 characters are used.
eventnoThe 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

FieldTypeDescription
verifiedbooleantrue: Shield has this session for your site, and the fields below describe it. false: it does not, see When verified is false.
risk_statusavailable or unknownavailable: at least one event of the session was scored. unknown: none had IP intelligence, so there is no score.
risk_scoreinteger 0 to 100, or nullThe highest risk score of any event in the session. null when risk_status is unknown.
recommendationallow, monitor, challenge or blockThe 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_passedbooleanThe visitor completed a Turnstile challenge that Shield verified for this session.
challenge_passed_attimestamp or nullWhen that happened.
sessionobjectThe 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 autoTrack on, 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>" }.

StatuserrorWhen
400invalid_jsonThe body is not valid JSON.
400session_id_requiredsession_id is missing, empty or not a string.
401invalid_secret_keyThe Authorization header is missing, is not Bearer followed by a key that starts with sec_, or the key belongs to no site.
429rate_limitedToo 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.
500internal_errorAn 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',
  }),
});