{
  "openapi": "3.1.0",
  "info": {
    "title": "Sentinel Fraud Detection API",
    "version": "1.4.0",
    "description": "Real-time fraud, VPN, residential-proxy, datacenter, antidetect-browser, and bot detection. Sub-40ms server-side global response. Free tier: 1,000 requests per hour per API key, no credit card.",
    "contact": {
      "name": "Sentinel Support",
      "email": "support@maskbreak.com",
      "url": "https://maskbreak.com/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://maskbreak.com/terms"
    },
    "termsOfService": "https://maskbreak.com/terms"
  },
  "externalDocs": {
    "description": "Full developer documentation",
    "url": "https://maskbreak.com/api"
  },
  "servers": [
    {
      "url": "https://maskbreak.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Evaluate",
      "description": "Score visitor sessions for fraud signals."
    },
    {
      "name": "Lookup",
      "description": "Verdict for an arbitrary IP address."
    },
    {
      "name": "Sandbox",
      "description": "Public, unauthenticated sample responses for development."
    },
    {
      "name": "Usage",
      "description": "Quota position and lifetime counters for the calling API key."
    },
    {
      "name": "Status",
      "description": "Service health and aggregate threat feed."
    },
    {
      "name": "Free tools",
      "description": "Unauthenticated lookup tools with tight per-IP limits."
    }
  ],
  "paths": {
    "/v1/lookup/{ip}": {
      "get": {
        "summary": "Look up an arbitrary IP address",
        "description": "Returns an allow/review/block verdict for any public IPv4 or IPv6 address: 0-100 risk score, VPN/proxy/Tor/datacenter/anonymity signals, and network attribution (ASN, organization, country). Shares the per-key hourly quota with /v1/evaluate.",
        "operationId": "lookupIp",
        "tags": [
          "Lookup"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "ip",
            "in": "path",
            "required": true,
            "description": "Public IPv4 or IPv6 address.",
            "schema": {
              "type": "string",
              "example": "185.220.101.34"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verdict for the IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResponse"
                },
                "examples": {
                  "torExit": {
                    "summary": "Tor exit node",
                    "value": {
                      "ip": "185.220.101.34",
                      "known": true,
                      "verdict": "block",
                      "risk_score": 90,
                      "signals": {
                        "vpn": false,
                        "proxied": false,
                        "tor": true,
                        "dch": true,
                        "anon": true
                      },
                      "network": {
                        "asn": 205100,
                        "org": "F3 Netze e.V.",
                        "country": "DE",
                        "city": null
                      },
                      "latency_ms": 38
                    }
                  },
                  "noReputationData": {
                    "summary": "IP not in our reputation feeds (known:false is not a clean guarantee)",
                    "value": {
                      "ip": "82.131.28.10",
                      "known": false,
                      "verdict": "allow",
                      "risk_score": 0,
                      "signals": null,
                      "network": null,
                      "latency_ms": 24
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Hourly request limit for this key",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window (advisory)",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds when the window resets",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Request-Id": {
                "description": "Correlator to quote to support",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid, private, or reserved IP."
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "429": {
            "description": "Rate limit exceeded. Retry-After header carries seconds until reset."
          },
          "403": {
            "description": "Forbidden - account suspended, or the caller's IP is not on the key's IP allowlist (the hint field names the calling address). Not retryable from the same address.",
            "headers": {
              "X-Request-Id": {
                "description": "Correlator to quote to support (set on every /v1 and /api response).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/evaluate": {
      "post": {
        "summary": "Evaluate a visitor session",
        "description": "Forward the client-side Sentinel token (auto-injected by the SDK into your forms) to receive a structured threat assessment. The response carries both the legacy fields (status / isSuspicious / details / deviceIntel) and the modern documented fields (decision / risk_score / network / device / reasons) so existing and new integrations both work.",
        "operationId": "evaluate",
        "tags": [
          "Evaluate"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EvaluateRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "Minimal request",
                  "value": {
                    "token": "mcl_eyJhbGciOi..."
                  }
                },
                "withFingerprint": {
                  "summary": "Request with optional Fingerprint event ID",
                  "value": {
                    "token": "mcl_eyJhbGciOi...",
                    "fingerprintEventId": "fp_event_a8c..."
                  }
                },
                "withAccountLinking": {
                  "summary": "Request with multi-account linking",
                  "value": {
                    "token": "mcl_eyJhbGciOi...",
                    "fingerprintEventId": "fp_event_a8c...",
                    "accountId": "user_8123"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Evaluation succeeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvaluateResponse"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Hourly request limit for this key",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window (advisory)",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds when the window resets",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Request-Id": {
                "description": "Correlator to quote to support",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "403": {
            "description": "Forbidden - account suspended, or the caller's IP is not on the key's IP allowlist (the hint field names the calling address). Not retryable from the same address.",
            "headers": {
              "X-Request-Id": {
                "description": "Correlator to quote to support (set on every /v1 and /api response).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/evaluate/sample": {
      "get": {
        "summary": "Sample evaluation response",
        "description": "Public, unauthenticated, rate-limited (30 req/min per IP). Returns a deterministic example /v1/evaluate response in one of six preset scenarios so you can wire your parsing logic without signing up. Uses the exact same response builder as the production endpoint — what you see here is what your code will receive in production.",
        "operationId": "evaluateSample",
        "tags": [
          "Sandbox"
        ],
        "security": [],
        "parameters": [
          {
            "name": "scenario",
            "in": "query",
            "required": false,
            "description": "Which preset scenario to return.",
            "schema": {
              "type": "string",
              "enum": [
                "clean",
                "vpn",
                "datacenter",
                "proxy",
                "tor",
                "bot"
              ],
              "default": "vpn"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sample evaluation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvaluateResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "summary": "Quota position for the calling key",
        "description": "Returns the calling key's hourly quota position and lifetime counters. Works with both key types — sk_live_ and sk_test_ each report their own hourly bucket, named in key_type. The call is free: it never consumes quota. used_this_hour and remaining are advisory — the counters are in-memory and per-process, so they reset on a deploy (enforcement of the actual limit is unaffected).",
        "operationId": "usage",
        "tags": [
          "Usage"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Quota position for the calling key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key_type": {
                      "type": "string",
                      "enum": [
                        "live",
                        "test"
                      ],
                      "description": "Which kind of key made the call. Each key type has its own hourly bucket."
                    },
                    "hourly_limit": {
                      "type": "integer",
                      "description": "Hourly request limit for this key.",
                      "example": 1000
                    },
                    "used_this_hour": {
                      "type": "integer",
                      "description": "Requests used in the current window. Advisory: in-memory and per-process, resets on deploy."
                    },
                    "remaining": {
                      "type": "integer",
                      "description": "Requests left in the current window. Advisory, like used_this_hour."
                    },
                    "resets_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the current hourly window resets. null when nothing has been used this window."
                    },
                    "total_evaluations": {
                      "type": "integer",
                      "description": "Lifetime evaluations recorded for the account."
                    },
                    "limit_hits": {
                      "type": "integer",
                      "description": "How many times the account has hit its hourly limit."
                    }
                  }
                },
                "examples": {
                  "liveKey": {
                    "summary": "Live key mid-window",
                    "value": {
                      "key_type": "live",
                      "hourly_limit": 1000,
                      "used_this_hour": 412,
                      "remaining": 588,
                      "resets_at": "2026-07-19T11:00:00.000Z",
                      "total_evaluations": 183204,
                      "limit_hits": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/status": {
      "get": {
        "summary": "Service health",
        "operationId": "status",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Status payload",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "operational"
                    },
                    "uptime": {
                      "type": "object",
                      "description": "Time since the current instance booted.",
                      "properties": {
                        "days": {
                          "type": "integer"
                        },
                        "hours": {
                          "type": "integer"
                        },
                        "minutes": {
                          "type": "integer"
                        },
                        "started": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "services": {
                      "type": "object",
                      "description": "Per-service health with rolling uptime_pct over the recent probe window."
                    },
                    "last_probe": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "version": {
                      "type": "string"
                    },
                    "history_90d": {
                      "type": "array",
                      "description": "Persisted per-day uptime for the last 90 days, oldest first. Present when daily history exists. Percentages are null for days without samples (per-service percentages are also null before per-service tracking began).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string",
                            "format": "date"
                          },
                          "pct": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "Overall uptime percentage for the day, 0-100."
                          },
                          "samples": {
                            "type": "integer",
                            "description": "Number of probes recorded that day."
                          },
                          "api_pct": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "dashboard_pct": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "authentication_pct": {
                            "type": [
                              "number",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/threat-feed": {
      "get": {
        "summary": "Aggregate threat counts",
        "description": "Public, anonymized aggregate counts plus live detection coverage: threats blocked in the last 24h (rolling window), weekly identity checks, developer count, and the current size of the datacenter/VPN range index and intel feeds. Counts cover authenticated API traffic only (/v1/evaluate, /v1/lookup, /mcp) and exclude test-key calls, so `today` can legitimately read 0 while `week_total` is large — which is why the marketing trust strip renders the `coverage` block, not volume. Coverage is fail-open: a feed that has not loaded reports 0.",
        "operationId": "threatFeed",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Aggregate counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "today": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "threats": {
                          "type": "integer"
                        },
                        "vpn": {
                          "type": "integer"
                        },
                        "proxy": {
                          "type": "integer"
                        },
                        "dch": {
                          "type": "integer"
                        }
                      }
                    },
                    "week_total": {
                      "type": "integer"
                    },
                    "week": {
                      "type": "object",
                      "description": "7-day aggregates. `total` mirrors week_total.",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "threats": {
                          "type": "integer"
                        }
                      }
                    },
                    "total_users": {
                      "type": "integer"
                    },
                    "coverage": {
                      "type": "object",
                      "description": "Live size of the detection data the verdict pipeline queries. 0 means that feed has not loaded (fail-open), never that coverage was lost.",
                      "properties": {
                        "dch_ranges": {
                          "type": "integer",
                          "description": "IPv4 + IPv6 datacenter/cloud prefixes currently indexed (ADR-0001)."
                        },
                        "cloud_providers": {
                          "type": "integer",
                          "description": "Provider range feeds loaded."
                        },
                        "tor_exits": {
                          "type": "integer",
                          "description": "Tor exit nodes currently listed (ADR-0002)."
                        },
                        "disposable_domains": {
                          "type": "integer",
                          "description": "Disposable-email domains currently listed (ADR-0002)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/lookup": {
      "post": {
        "summary": "Free single-IP lookup (no key)",
        "description": "The free tool behind the homepage scanner and /ip-lookup. Same verdict pipeline as /v1/lookup but unauthenticated and rate-limited to 12/minute and 80/day per caller IP. For production traffic use /v1/lookup with an API key.",
        "operationId": "freeLookup",
        "tags": [
          "Free tools"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ip"
                ],
                "properties": {
                  "ip": {
                    "type": "string",
                    "description": "Public IPv4 or IPv6 address.",
                    "example": "185.220.101.34"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verdict for the IP (same shape as /v1/lookup).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Free-tool rate limit reached (12/min or 80/day per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/lookup-bulk": {
      "post": {
        "summary": "Free bulk IP check (email-gated)",
        "description": "Check up to 20 unique IPs in one call. Requires a contact email (subscribes it to the monthly Fraud Brief; unsubscribe any time). Limited to 3 batches/hour per caller IP. For sustained bulk use, call /v1/lookup with an API key.",
        "operationId": "freeLookupBulk",
        "tags": [
          "Free tools"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ips",
                  "email"
                ],
                "properties": {
                  "ips": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "string"
                    },
                    "description": "Up to 20 public IP addresses (duplicates and invalid entries are skipped)."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Where to reach you; added to the monthly Fraud Brief."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-IP verdicts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "description": "IPs evaluated."
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Invalid or duplicate entries dropped."
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LookupResponse"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing email or no valid IPs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Bulk limit reached (3 batches/hour per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_live_...",
        "description": "Pass your Sentinel API key from https://maskbreak.com/dashboard as a Bearer token. Live keys start with sk_live_. Every account also has a personal test key starting with sk_test_ (Settings -> API Key): it runs the complete live pipeline but events are flagged test, nothing bills, no webhooks fire, and it is exempt from the account's IP allowlist — responses carry test: true. Additionally, the public sandbox key sk_test_sandbox works with no account at all, but only answers the deterministic test_* tokens."
      }
    },
    "schemas": {
      "LookupResponse": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string",
            "description": "The IP that was looked up."
          },
          "known": {
            "type": "boolean",
            "description": "Whether our network-reputation feeds hold data for this IP. false means the IP is not in our datacenter, Tor, or ASN reputation sets — it does NOT assert the IP is clean. Do not treat known:false as a safe signal; for VPN/proxy/device analysis of a live visitor, use POST /v1/evaluate with a client token."
          },
          "verdict": {
            "type": "string",
            "enum": [
              "allow",
              "review",
              "block"
            ]
          },
          "risk_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "signals": {
            "type": [
              "object",
              "null"
            ],
            "description": "Null when known is false.",
            "properties": {
              "vpn": {
                "type": "boolean"
              },
              "proxied": {
                "type": "boolean",
                "description": "Proxy detected (legacy field spelling)."
              },
              "tor": {
                "type": "boolean"
              },
              "dch": {
                "type": "boolean",
                "description": "Datacenter hosting (legacy field spelling)."
              },
              "anon": {
                "type": "boolean",
                "description": "Anonymizing behaviors observed."
              }
            }
          },
          "network": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "asn": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "org": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "latency_ms": {
            "type": "integer"
          },
          "engine_verdict": {
            "type": "string",
            "enum": [
              "allow",
              "review",
              "block"
            ],
            "description": "Present only when an exception pin changed `verdict`: the engine's own verdict."
          },
          "verdict_source": {
            "type": "string",
            "enum": [
              "exception"
            ],
            "description": "Present when an explicit per-IP exception pin authored the verdict."
          },
          "exception_matched": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "test": {
            "type": "boolean",
            "description": "Present when the call used the per-account sk_test_ key."
          }
        }
      },
      "EvaluateRequest": {
        "type": "object",
        "required": [
          "token"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Client-side token captured from the Sentinel SDK (auto-injected as a hidden input named 'monocle' on any form with class 'monocle-enriched'). Deterministic test tokens (test_clean, test_vpn, test_proxy, test_datacenter, test_tor) return canned responses flagged test:true — never billed, stored, or webhooked.",
            "example": "mcl_eyJhbGciOi..."
          },
          "fingerprintEventId": {
            "type": "string",
            "description": "Optional device-intelligence event id for additional device signals.",
            "example": "fp_event_a8c..."
          },
          "accountId": {
            "type": "string",
            "maxLength": 256,
            "description": "Optional: your own account/user identifier for this session. When provided together with fingerprintEventId, Sentinel counts how many distinct accounts this device has been linked to for your API key (multi-accounting detection). Stored only as a one-way hash.",
            "example": "user_8123"
          },
          "email": {
            "type": "string",
            "maxLength": 254,
            "description": "Optional: the email the visitor is signing up with. Adds email.disposable to the response, checked against a continuously refreshed disposable-domain feed. A hit adds the disposable_email reason, raises risk_score, and escalates allow to review. Checked transiently — never stored or logged.",
            "example": "visitor@mailinator.com"
          }
        }
      },
      "NetworkSignals": {
        "type": "object",
        "description": "Network-layer signals derived from Sentinel's network intelligence engine.",
        "properties": {
          "vpn": {
            "type": "boolean",
            "description": "Commercial VPN tunnel detected (NordVPN, Proton, ExpressVPN, etc.)."
          },
          "proxy": {
            "type": "boolean",
            "description": "Residential or SOCKS/HTTP proxy detected."
          },
          "datacenter": {
            "type": "boolean",
            "description": "Traffic originates from a known datacenter / cloud provider ASN."
          },
          "anonymous": {
            "type": "boolean",
            "description": "Any anonymization layer detected."
          },
          "tor": {
            "type": "boolean",
            "description": "Tor exit node detected."
          },
          "residential": {
            "type": "boolean",
            "description": "True when neither datacenter nor proxy flags fired."
          },
          "service": {
            "type": [
              "string",
              "null"
            ],
            "description": "Identified provider name when known (e.g. PROTON_VPN, BRIGHT_DATA).",
            "example": "PROTON_VPN"
          }
        }
      },
      "DeviceSignals": {
        "type": "object",
        "description": "Device-layer signals derived from Sentinel's device intelligence engine. Only present when fingerprintEventId was sent on the request.",
        "properties": {
          "antidetect": {
            "type": "boolean",
            "description": "Antidetect / fingerprint-spoofing browser (Multilogin, Kameleo, GoLogin, etc.)."
          },
          "automation": {
            "type": "boolean",
            "description": "Automated browser detected (Puppeteer, Playwright, Selenium)."
          },
          "emulator": {
            "type": "boolean",
            "description": "Mobile emulator."
          },
          "virtual_machine": {
            "type": "boolean",
            "description": "Virtual machine."
          },
          "incognito": {
            "type": "boolean",
            "description": "Private / incognito browsing."
          },
          "privacy_mode": {
            "type": "boolean",
            "description": "Browser privacy / anti-tracking mode."
          },
          "ip_blocklisted": {
            "type": "boolean",
            "description": "IP appears on a known threat blocklist."
          },
          "high_activity": {
            "type": "boolean",
            "description": "This visitor ID is being identified unusually often (High-Activity Device). Informational — adds reason code high_activity_device and a small risk bump, but never flips the decision on its own."
          },
          "visitor_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Stable hash of the device fingerprint. Same device returns the same id across sessions, even after cookies clear.",
            "example": "v_a8c91b..."
          },
          "tampering_score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "0–1 tampering score. Above 0.6 strongly suggests an antidetect browser; 0.3–0.6 means soft inconsistencies; below 0.3 is normal.",
            "example": 0.27
          },
          "times_seen": {
            "type": "integer",
            "description": "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). Present only when the request carried a fingerprintEventId that resolved to a visitor id.",
            "example": 7
          },
          "first_seen": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601 timestamp of when Sentinel first saw this device under your API key. Present only when the request carried a fingerprintEventId that resolved to a visitor id.",
            "example": "2026-05-02T11:26:14.000Z"
          },
          "returning": {
            "type": "boolean",
            "description": "True when times_seen > 1 - this device has been evaluated before.",
            "example": true
          },
          "linked_accounts": {
            "type": "integer",
            "description": "Distinct accounts (your accountId values) this device has been seen with under your API key in the last 90 days. Present only when accountId and fingerprintEventId were both provided.",
            "example": 3
          },
          "multi_account": {
            "type": "boolean",
            "description": "True when linked_accounts > 1 - the core multi-accounting signal. Adds reason code multi_account_device.",
            "example": true
          }
        }
      },
      "EvaluateResponse": {
        "type": "object",
        "description": "Sentinel evaluation result. Carries both legacy fields and the modern documented fields.",
        "required": [
          "status",
          "isSuspicious",
          "details",
          "decision",
          "risk_score",
          "network",
          "reasons"
        ],
        "properties": {
          "status": {
            "type": "string",
            "example": "success",
            "description": "Always 'success' on a 2xx response."
          },
          "isSuspicious": {
            "type": "boolean",
            "description": "Master flag. true when any threat signal fired."
          },
          "details": {
            "type": "object",
            "description": "Raw network signals (legacy shape, kept for backwards compatibility).",
            "properties": {
              "ip": {
                "type": "string",
                "example": "198.51.100.18"
              },
              "cc": {
                "type": "string",
                "example": "NL",
                "description": "ISO 3166-1 alpha-2 country code."
              },
              "vpn": {
                "type": "boolean"
              },
              "proxied": {
                "type": "boolean"
              },
              "anon": {
                "type": "boolean"
              },
              "dch": {
                "type": "boolean",
                "description": "Datacenter ASN flag."
              },
              "tor": {
                "type": "boolean"
              },
              "service": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "PROTON_VPN"
              }
            }
          },
          "deviceIntel": {
            "type": [
              "object",
              "null"
            ],
            "description": "Raw Fingerprint Smart Signals (legacy shape). Only present when fingerprintEventId was sent.",
            "properties": {
              "visitorId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "browserTampering": {
                "type": "boolean"
              },
              "tamperingScore": {
                "type": "number"
              },
              "botDetected": {
                "type": "boolean"
              },
              "vpnDetected": {
                "type": "boolean"
              },
              "proxyDetected": {
                "type": "boolean"
              },
              "torDetected": {
                "type": "boolean"
              },
              "ipBlocklisted": {
                "type": "boolean"
              },
              "incognito": {
                "type": "boolean"
              },
              "emulator": {
                "type": "boolean"
              },
              "virtualMachine": {
                "type": "boolean"
              },
              "privacySettings": {
                "type": "boolean"
              },
              "highActivity": {
                "type": "boolean"
              }
            }
          },
          "decision": {
            "type": "string",
            "enum": [
              "allow",
              "review",
              "block"
            ],
            "description": "Recommended action. 'block' fires on hard signals (proxy, Tor, antidetect, automation, emulator). 'review' on soft signals. 'allow' otherwise."
          },
          "risk_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Composite risk score across all network and device signals.",
            "example": 71
          },
          "ip": {
            "type": [
              "string",
              "null"
            ],
            "example": "198.51.100.18"
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "example": "NL",
            "description": "ISO 3166-1 alpha-2 country code."
          },
          "network": {
            "$ref": "#/components/schemas/NetworkSignals"
          },
          "device": {
            "$ref": "#/components/schemas/DeviceSignals"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Machine-readable reason codes for the decision.",
            "example": [
              "vpn_detected",
              "datacenter_asn"
            ]
          },
          "evaluated_in_ms": {
            "type": "integer",
            "example": 28,
            "description": "Server-side processing time for this request."
          },
          "engine_decision": {
            "type": "string",
            "enum": [
              "allow",
              "review",
              "block"
            ],
            "description": "Present only when your own policy (a rule or an exception pin) changed `decision`: the engine's own verdict."
          },
          "decision_source": {
            "type": "string",
            "enum": [
              "rules",
              "exception"
            ],
            "description": "Present whenever your policy matched this event: \"rules\" for a signal rule, \"exception\" for a per-IP/visitor pin. When the match changed the decision, engine_decision also appears with the engine's own verdict."
          },
          "rule_matched": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Signals that triggered a custom rule, when decision_source is \"rules\"."
          },
          "exception_matched": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The matched pins (e.g. \"ip:203.0.113.7\"), when decision_source is \"exception\". Pins outrank signal rules."
          },
          "email": {
            "type": "object",
            "properties": {
              "disposable": {
                "type": "boolean"
              }
            },
            "description": "Present when the optional email parameter was sent: whether the address uses a disposable/burner domain."
          },
          "test": {
            "type": "boolean",
            "description": "Present on responses to test tokens or per-account sk_test_ keys - never billed, stored (test-key events are stored but flagged), or webhooked."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "Invalid token."
          },
          "hint": {
            "type": "string",
            "description": "Optional, additive: a human-readable pointer to the likely fix (e.g. the exact header to send, or the calling IP to add to your allowlist). The error string itself is stable and machine-parseable; hint text may change.",
            "example": "Send your key as 'Authorization: Bearer sk_live_...'. Headers like X-API-Key and query params are not read."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Optional, additive: link to the relevant documentation section.",
            "example": "https://maskbreak.com/api#auth"
          }
        }
      },
      "WebhookThreatEvent": {
        "type": "object",
        "description": "Delivered to your configured HTTPS endpoint on engine threats and rule/exception blocks. Signed: X-Sentinel-Signature = 'sha256=' + HMAC_SHA256(secret, X-Sentinel-Timestamp + '.' + rawBody). Each event is attempted up to 3 times (retries after ~1 minute and ~8 minutes on failure), best-effort in-process — a deploy mid-retry drops the remaining attempts, so the dashboard Events log is the source of truth. Retried deliveries reuse the same event_id — dedupe on it. The dashboard keeps a 30-day delivery log.",
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "threat.detected",
              "test"
            ],
            "description": "'test' is sent by the dashboard Send-test button through the same delivery path — handle both."
          },
          "event_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Unique per event (32 hex chars; test events included), identical across retry attempts, and also sent as the X-Sentinel-Event-Id header on every delivery. Dedupe on it.",
            "example": "9f2ce7a4c1d84b7fa3d2c05e8b6f4a19"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "ip": {
            "type": "string"
          },
          "threat_type": {
            "type": "string",
            "example": "Tor",
            "description": "Human label: signal name, 'Custom rule (…)', or 'Customer exception (…)'."
          },
          "details": {
            "type": "object",
            "description": "Signal booleans (vpn, proxied, tor, dch, bot, tampering, antidetect), decision, and rule_matched/exception_matched attribution."
          }
        },
        "required": [
          "event",
          "event_id",
          "timestamp",
          "ip",
          "threat_type"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request body",
        "headers": {
          "X-Request-Id": {
            "description": "Correlator to quote to support (set on every /v1 and /api response).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "headers": {
          "X-Request-Id": {
            "description": "Correlator to quote to support (set on every /v1 and /api response).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Free tier limit is 1,000 requests per hour per API key, plus a 50,000 req/hour per-source-IP backstop.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Hourly request limit for this key. Present when a per-key quota produced the 429 (absent on the per-IP backstop limiters).",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests left in the current window (advisory). Present when a per-key quota produced the 429.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds when the window resets. Present when a per-key quota produced the 429.",
            "schema": {
              "type": "integer"
            }
          },
          "X-Request-Id": {
            "description": "Correlator to quote to support (set on every /v1 and /api response).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "threatDetected": {
      "post": {
        "summary": "Threat alert pushed to your endpoint",
        "description": "Configured in the dashboard Tools drawer. Slack/Discord URLs receive a channel-formatted message instead of this JSON shape.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookThreatEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answer 2xx within 10 seconds; redirects are never followed. A non-2xx answer or timeout schedules a retry (~1 minute, then ~8 minutes) with the same event_id."
          }
        }
      }
    }
  }
}
