{
  "openapi": "3.1.0",
  "info": {
    "title": "FindIP Shield Server Verification API",
    "version": "1.0.0",
    "summary": "Confirm a browser session's risk from your backend before you act on it.",
    "description": "One endpoint. Your backend sends the Shield session ID it received from the browser, authenticated with the site's secret key, and gets back the risk Shield recorded for that session. Browser-side results can be altered by the visitor; this response cannot.\n\nThe result is a set of risk signals and a recommendation for your code to use. It is an assessment, not proof of abuse, and Shield takes no action on its own.\n\nGuide: https://www.findip.net/docs/shield/server-verification",
    "contact": {
      "name": "FindIP",
      "url": "https://www.findip.net/Home/Contact"
    }
  },
  "externalDocs": {
    "description": "Server verification guide",
    "url": "https://www.findip.net/docs/shield/server-verification"
  },
  "servers": [
    {
      "url": "https://shield.findip.net"
    }
  ],
  "security": [
    {
      "siteSecretKey": []
    }
  ],
  "paths": {
    "/v1/shield/sessions/verify": {
      "post": {
        "operationId": "verifyShieldSession",
        "summary": "Verify a session's risk",
        "description": "Returns the risk Shield has recorded for one session of the site that owns the secret key.\n\nA session Shield has never seen for this site is not an error: the response is `200` with `verified: false`.\n\nChecks run in this order: the `Authorization` header format (`401`), the rate limit (`429`), the secret key lookup (`401`), the JSON body (`400`), then `session_id` (`400`). Call it from a server only; the endpoint sends no CORS headers and the secret key must never reach a browser.",
        "requestBody": {
          "required": true,
          "description": "A JSON object. The body is parsed as JSON whatever the `Content-Type` header says.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              },
              "example": {
                "session_id": "sess_xxx",
                "event": "signup_attempt"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session was looked up. Read `verified` first: `true` means Shield has this session for the site and the remaining fields describe it; `false` means it does not.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/VerifiedSession"
                    },
                    {
                      "$ref": "#/components/schemas/UnverifiedSession"
                    }
                  ]
                },
                "examples": {
                  "verified": {
                    "summary": "Known session with a score",
                    "value": {
                      "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
                        }
                      }
                    }
                  },
                  "verifiedWithoutIntelligence": {
                    "summary": "Known session, no IP intelligence for any of its events",
                    "value": {
                      "verified": true,
                      "risk_status": "unknown",
                      "risk_score": null,
                      "recommendation": "monitor",
                      "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": [],
                        "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": false,
                          "proxy": false,
                          "tor": false,
                          "relay": false,
                          "hosting": false,
                          "datacenter": false,
                          "malicious": false
                        }
                      }
                    }
                  },
                  "unverified": {
                    "summary": "Session not known for this site",
                    "value": {
                      "verified": false,
                      "risk_score": null,
                      "recommendation": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`: the body is not valid JSON. `session_id_required`: `session_id` is missing, empty or not a string.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_json": {
                    "value": {
                      "error": "invalid_json"
                    }
                  },
                  "session_id_required": {
                    "value": {
                      "error": "session_id_required"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`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. A public site key (`pub_...`) is rejected here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_secret_key": {
                    "value": {
                      "error": "invalid_secret_key"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many verify requests with this secret key in the current one-minute window. The limit is per key and separate from the browser SDK's event limit; the server default is 1,000 requests per minute. Requests with a well-formed but unknown key count too. No `Retry-After` header is sent; retry in the next minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limited": {
                    "value": {
                      "error": "rate_limited"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: an unexpected failure, for example the session store is unavailable. A body that is valid JSON but not an object, such as `null`, also ends here. Treat it as \"risk unknown\", not as safe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal_error": {
                    "value": {
                      "error": "internal_error"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "siteSecretKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sec_...",
        "description": "The site's secret key from the Shield dashboard, sent as `Authorization: Bearer sec_...`. The scheme name is matched exactly as `Bearer`. Keep the key on your server."
      }
    },
    "schemas": {
      "VerifyRequest": {
        "type": "object",
        "required": [
          "session_id"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "minLength": 1,
            "description": "The Shield session ID from the browser: `FindIP.getSession().sessionId`, the `findip_session` form field added by the SDK's `sessionField` option, or the `_fip_sid` cookie. Only the first 128 characters are used."
          },
          "event": {
            "type": "string",
            "description": "Optional. 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.",
            "examples": [
              "signup_attempt"
            ]
          }
        },
        "description": "Other properties are ignored."
      },
      "Recommendation": {
        "type": "string",
        "enum": [
          "allow",
          "monitor",
          "challenge",
          "block"
        ],
        "description": "What Shield suggests for the session. `allow`: nothing notable. `monitor`: let it through and keep an eye on it. `challenge`: ask for an extra check before continuing. `block`: refuse. It is a suggestion for your code; Shield does not enforce it here."
      },
      "VerifiedSession": {
        "type": "object",
        "required": [
          "verified",
          "risk_status",
          "risk_score",
          "recommendation",
          "challenge_passed",
          "challenge_passed_at",
          "session"
        ],
        "properties": {
          "verified": {
            "const": true,
            "description": "Shield has recorded this session for the site that owns the key."
          },
          "risk_status": {
            "type": "string",
            "enum": [
              "available",
              "unknown"
            ],
            "description": "`available`: at least one event of the session was scored. `unknown`: none of its events had IP intelligence, so there is no score. Treat `unknown` as `monitor`, never as safe."
          },
          "risk_score": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "The highest risk score of any event in the session, 0 to 100. `null` when `risk_status` is `unknown`."
          },
          "recommendation": {
            "$ref": "#/components/schemas/Recommendation",
            "description": "The recommendation Shield gave for the session's most recent event. When none is stored it is `allow` if the session has a score and `monitor` if it does not. Never `null` when `verified` is `true`."
          },
          "challenge_passed": {
            "type": "boolean",
            "description": "`true` once the visitor completed a Cloudflare Turnstile challenge that Shield verified for this session."
          },
          "challenge_passed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When that challenge was passed (ISO 8601, UTC), or `null`."
          },
          "session": {
            "$ref": "#/components/schemas/Session"
          }
        }
      },
      "UnverifiedSession": {
        "type": "object",
        "required": [
          "verified",
          "risk_score",
          "recommendation"
        ],
        "properties": {
          "verified": {
            "const": false,
            "description": "Shield has no session with this ID for the site that owns the key. Typical causes: the SDK never ran or was blocked in that browser, the ID was invented or altered, or it belongs to another site. It says nothing about risk."
          },
          "risk_score": {
            "type": "null"
          },
          "recommendation": {
            "type": "null"
          }
        },
        "description": "Exactly these three fields are returned: there is no `risk_status`, `challenge_passed` or `session`. Decide in your own code what an unverifiable session may do; do not read it as `allow`."
      },
      "Session": {
        "type": "object",
        "description": "What Shield has recorded for the whole session, for your own records. It carries no user identity.",
        "required": [
          "platform",
          "visitor_id",
          "first_seen_at",
          "last_seen_at",
          "event_count",
          "pageview_count",
          "risk_reasons",
          "ip_rotating",
          "asn_rotating",
          "country_rotating",
          "distinct_ips",
          "distinct_asns",
          "distinct_countries",
          "ips",
          "countries",
          "last_ip",
          "last_country",
          "last_country_name",
          "last_asn",
          "last_asn_name",
          "last_organization",
          "network"
        ],
        "properties": {
          "platform": {
            "type": "string",
            "enum": [
              "web",
              "webview",
              "ios",
              "android"
            ],
            "description": "Where the session's most recent event came from: `web` for a website, `webview` for a page inside a mobile app, `ios` or `android` for a native app. Added with mobile app sites; a session from a website is always `web`."
          },
          "visitor_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Shield's visitor ID for the browser, or `null` when none was recorded."
          },
          "first_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Time of the session's first event (ISO 8601, UTC)."
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Time of the session's most recent event (ISO 8601, UTC)."
          },
          "event_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Events recorded in the session."
          },
          "pageview_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Of those, page views."
          },
          "risk_reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The distinct reason codes across every event of the session, in no particular order. Network reasons are `tor_detected`, `vpn_detected`, `proxy_detected`, `relay_detected`, `hosting_network` and `malicious_ip`. A sensitive event adds its own name: `signup_attempt`, `signup_success`, `login_attempt`, `login_failed`, `payment_attempt`, `checkout_started` or `lead_submitted`. Other codes can appear; treat the list as open."
          },
          "ip_rotating": {
            "type": "boolean",
            "description": "`true` when the session was seen from more than one IP address."
          },
          "asn_rotating": {
            "type": "boolean",
            "description": "`true` when it was seen from more than one network (ASN)."
          },
          "country_rotating": {
            "type": "boolean",
            "description": "`true` when it was seen from more than one country."
          },
          "distinct_ips": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of distinct IP addresses."
          },
          "distinct_asns": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of distinct ASNs."
          },
          "distinct_countries": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of distinct countries."
          },
          "ips": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string"
            },
            "description": "Up to 10 of the distinct IP addresses, IPv4 or IPv6."
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The distinct country codes seen (two letters)."
          },
          "last_ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "IP address of the most recent event."
          },
          "last_country": {
            "type": [
              "string",
              "null"
            ],
            "description": "Country code of the most recent event."
          },
          "last_country_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Country name of the most recent event."
          },
          "last_asn": {
            "type": [
              "integer",
              "null"
            ],
            "description": "ASN of the most recent event."
          },
          "last_asn_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of that ASN."
          },
          "last_organization": {
            "type": [
              "string",
              "null"
            ],
            "description": "Organization the most recent IP address is registered to."
          },
          "network": {
            "$ref": "#/components/schemas/NetworkFlags"
          }
        }
      },
      "NetworkFlags": {
        "type": "object",
        "description": "Each flag is `true` if any event of the session came from that kind of address.",
        "required": [
          "vpn",
          "proxy",
          "tor",
          "relay",
          "hosting",
          "datacenter",
          "malicious"
        ],
        "properties": {
          "vpn": {
            "type": "boolean"
          },
          "proxy": {
            "type": "boolean"
          },
          "tor": {
            "type": "boolean"
          },
          "relay": {
            "type": "boolean"
          },
          "hosting": {
            "type": "boolean"
          },
          "datacenter": {
            "type": "boolean"
          },
          "malicious": {
            "type": "boolean"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_json",
              "session_id_required",
              "invalid_secret_key",
              "rate_limited",
              "internal_error"
            ],
            "description": "A stable machine-readable code."
          }
        }
      }
    }
  }
}
