{
  "openapi": "3.1.0",
  "info": {
    "title": "Maskbreak Fraud Detection API",
    "version": "1.8.0",
    "description": "Real-time fraud, VPN, residential-proxy, datacenter, antidetect-browser, and bot detection. Use live visitor evaluation for network and device signals; bare-IP lookups have more limited coverage. Plans (since 2026-09-30): Free — 10,000 visitor checks a month, up to 1,000 an hour, no credit card; Starter, Growth and Scale from €29 to €299 a month (excluding VAT); Enterprise by contract (https://maskbreak.com/pricing). A visitor check is one /v1/evaluate call; /v1/lookup has its own monthly allowance of 10× the plan's visitor checks. A used-up monthly allowance answers 429 with code monthly_quota_exceeded. Processing time is returned per evaluation; end-to-end latency depends on the request and available intelligence sources.",
    "contact": {
      "name": "Maskbreak 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": []
    },
    {
      "oauth2": [
        "api"
      ]
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Obtaining and using credentials."
    },
    {
      "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/device-evidence/challenge": {
      "post": {
        "tags": ["Evaluate"],
        "operationId": "createDeviceEvidenceChallenge",
        "summary": "Issue an optional WebGPU beta challenge (backend only)",
        "description": "Requires an administrator-enabled beta, a live account key, and a provider-verified device event less than five minutes old from the website configured in the dashboard. Challenge expires after two minutes and is bound to customer, event and device. Maximum 60 challenges per customer per hour and 50 outstanding. No usage charge, email, or fraud decision. Never expose the API key to the browser. Test keys are not accepted.",
        "security": [{"bearerAuth": []}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {
          "type": "object", "required": ["fingerprintEventId"], "properties": {
            "fingerprintEventId": {"type": "string", "minLength": 8, "maxLength": 200}
          }
        }}}},
        "responses": {
          "200": {"description": "One-use challenge", "content": {"application/json": {"schema": {
            "type": "object", "required": ["challenge", "expires_at", "protocol", "experimental"], "properties": {
              "challenge": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
              "expires_at": {"type": "string", "format": "date-time"},
              "protocol": {"const": "atomic-order-group-median-v1:128x128:32:low-power"},
              "consistency_protocol": {"const": "browser-consistency-v1"},
              "experimental": {"const": true}
            }
          }}}},
          "400": {"description": "Missing or invalid fingerprintEventId"},
          "401": {"description": "A live account key is required"},
          "403": {"description": "Suspended account or IP allowlist restriction"},
          "409": {"description": "Beta disabled or outstanding-challenge cap reached"},
          "422": {"description": "Missing, old or wrong-origin device evidence"},
          "429": {"description": "Rate limit reached"},
          "503": {"description": "Optional evidence unavailable; continue normal checks"}
        }
      }
    },
    "/v1/lookup/{ip}": {
      "get": {
        "summary": "Look up an arbitrary IP address",
        "description": "Returns an allow/review/block verdict and 0-100 risk score for a public IP. A bare lookup checks two things: Tor exit lists (tor, block at 90) and published cloud-server ranges (dch, review at 40). When the public routing table knows the address, `network` also carries its ASN, AS name and AS registration country with `source: \"asn_table\"`; that label is for display only and never changes the verdict or score. `country` and `city` stay null on a bare lookup (the registration country is where the network is registered, not where a visitor is). known:false means neither list matched, not that the IP is safe. VPN and proxy verdicts need a live visit: use POST /v1/evaluate with the browser SDK evidence. Shares the account's hourly quota with /v1/evaluate.",
        "operationId": "lookupIp",
        "tags": [
          "Lookup"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "api"
            ]
          }
        ],
        "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 (Tor exit list match)",
                    "value": {
                      "ip": "185.220.101.34",
                      "known": true,
                      "verdict": "block",
                      "risk_score": 90,
                      "signals": {
                        "vpn": false,
                        "proxied": false,
                        "tor": true,
                        "dch": false,
                        "anon": true
                      },
                      "network": {
                        "asn": 60729,
                        "org": "Tor exit node",
                        "country": null,
                        "city": null,
                        "registration_country": "DE",
                        "source": "asn_table"
                      },
                      "latency_ms": 12
                    }
                  },
                  "noReputationData": {
                    "summary": "Neither list matched (known:false is not a clean guarantee); the ASN label is display only",
                    "value": {
                      "ip": "24.48.0.1",
                      "known": false,
                      "verdict": "allow",
                      "risk_score": 0,
                      "signals": null,
                      "network": {
                        "asn": 5769,
                        "org": "VIDEOTRON",
                        "country": null,
                        "city": null,
                        "registration_country": "CA",
                        "source": "asn_table"
                      },
                      "latency_ms": 9
                    }
                  }
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Hourly request limit for this key. Omitted for uncapped public-interest keys.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window (advisory). Omitted for uncapped public-interest keys.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds when the window resets. Omitted for uncapped public-interest keys.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Request-Id": {
                "description": "Correlator to quote to support",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The path is not one public IPv4 or IPv6 address: `Invalid IP format.` or `Private or reserved IP — nothing to look up.` The second also answers an IPv4-mapped address (`::ffff:` followed by the IPv4): send the IPv4 alone."
          },
          "401": {
            "description": "Missing or invalid API key. An empty key (`Authorization: Bearer` with nothing after it, which is what an empty key variable sends) answers `Missing Key.`; the hint field names the likely cause."
          },
          "503": {
            "$ref": "#/components/responses/StorageUnavailable"
          },
          "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": {
      "post": {
        "summary": "Evaluate a visitor session",
        "description": "Forward the client-side Maskbreak token for full network intelligence. With an account key or OAuth token, a missing, null or empty client token is accepted as a degraded HTTP fallback: the visitor's address is then unknown (ip: \"unknown\"), so the network layer, the Tor exit list and the cloud ranges cannot run, and the answer is allow with degraded: true unless device, email or policy signals say otherwise. Without a retrievable device event the decision comes from the network layer alone; device is omitted and degraded is not set. The public sandbox requires a deterministic test token. Official server SDKs currently require a non-empty token. Send the body as application/json; since 28 September 2026 a JSON object sent as text/plain, application/x-www-form-urlencoded, application/*+json or without a Content-Type is read the same way (before, such a body was ignored and the call answered as if no token had been sent), and a form body sent as application/x-www-form-urlencoded (token=…&fingerprintEventId=…&email=…&tz=…) gives those string fields. A multipart body is not read. The Bearer scheme name is case-insensitive. The response carries both legacy fields (status / isSuspicious / details / deviceIntel) and modern fields (decision / risk_score / network / device / reasons).",
        "operationId": "evaluate",
        "tags": [
          "Evaluate"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "api"
            ]
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EvaluateRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "Minimal request",
                  "value": {
                    "token": "eyJhbGciOi..."
                  }
                },
                "withDeviceEvent": {
                  "summary": "Request with optional device event ID",
                  "value": {
                    "token": "eyJhbGciOi...",
                    "fingerprintEventId": "fp_event_a8c..."
                  }
                },
                "withAccountLinking": {
                  "summary": "Request with multi-account linking",
                  "value": {
                    "token": "eyJhbGciOi...",
                    "fingerprintEventId": "fp_event_a8c...",
                    "accountId": "user_8123"
                  }
                },
                "withoutNetworkToken": {
                  "summary": "Degraded HTTP fallback with an account credential (not the public sandbox)",
                  "value": {}
                },
                "withTimezone": {
                  "summary": "Deterministic timezone check",
                  "value": {
                    "token": "test_clean",
                    "tz": "Europe/Tallinn"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Evaluation succeeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvaluateResponse"
                }
              }
            },
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Hourly request limit for this key. Omitted for uncapped public-interest keys and the public sandbox.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window (advisory). Omitted for uncapped public-interest keys and the public sandbox.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds when the window resets. Omitted for uncapped public-interest keys and the public sandbox.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Request-Id": {
                "description": "Correlator to quote to support",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid token - the network-intelligence provider rejected the client token (malformed or tampered), or a non-null token has the wrong type. Not counted in total_evaluations; still uses one request of the account key's hourly rate limit. With an account credential, a missing, null or empty token is answered fail-open with degraded: true. The public sandbox instead returns 400 unless token is a supported test_* value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/StorageUnavailable"
          },
          "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, its plan and calendar-month allowance (additive since 2026-09-30: plan, monthly_limit, month_used, monthly_lookup_limit, month_lookups_used, month_resets_at, grandfathered_until), and durable account totals. Live and personal test keys report separate hourly buckets; OAuth tokens resolve to the current key of the corresponding type. The call consumes no evaluation quota. Hourly counters and enforcement are in-memory and per-process, so they reset on restart. Approved public-interest keys return hourly_limit and remaining as null with uncapped: true. This endpoint checks suspension but does not enforce the IP allowlist. It has a separate 120 requests/minute/IP limit and shares the invalid-key lockout, including for uncapped accounts.",
        "operationId": "usage",
        "tags": [
          "Usage"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "api"
            ]
          }
        ],
        "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",
                        "null"
                      ],
                      "description": "Hourly request limit for this key, or null when uncapped.",
                      "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",
                        "null"
                      ],
                      "description": "Requests left in the current window, or null when uncapped. Advisory, like used_this_hour."
                    },
                    "uncapped": {
                      "type": "boolean",
                      "description": "True for an approved public-interest key with no hourly cap. Independent per-IP limiters still apply to this endpoint."
                    },
                    "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 count of the account's counted /v1/evaluate, /v1/lookup and MCP lookup_ip calls (test-key calls, console checks and test tokens are not included)."
                    },
                    "limit_hits": {
                      "type": "integer",
                      "description": "How many times the account has hit its hourly limit."
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "starter",
                        "growth",
                        "scale",
                        "custom",
                        "public_interest"
                      ],
                      "description": "The account's plan. custom = an individually agreed quota or an Enterprise contract. Added 2026-09-30 (additive)."
                    },
                    "monthly_limit": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Visitor checks (/v1/evaluate calls) allowed this calendar month (UTC), e.g. 10000 on Free. Added 2026-09-30 (additive).",
                      "example": 10000
                    },
                    "month_used": {
                      "type": "integer",
                      "description": "Visitor checks counted this calendar month (UTC), test-key calls and console checks included (they run the full detection). The fixed test_* tokens and the sandbox key are never counted. Added 2026-09-30 (additive)."
                    },
                    "monthly_lookup_limit": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "/v1/lookup and MCP lookup_ip calls allowed this calendar month: 10 times the plan's visitor checks (100000 on Free). Added 2026-09-30 (additive).",
                      "example": 100000
                    },
                    "month_lookups_used": {
                      "type": "integer",
                      "description": "/v1/lookup and MCP lookup_ip calls counted this calendar month (UTC), test-key calls included. Added 2026-09-30 (additive)."
                    },
                    "month_resets_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the monthly allowances reset: 00:00 UTC on the 1st of next month. Added 2026-09-30 (additive)."
                    },
                    "grandfathered_until": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "For an account created before 2026-10-01 that still has its previous allowance (1,000 checks an hour, no monthly cap), when that allowance ends (the end of 30 November 2026; it read the end of 31 October until 2026-10-19); otherwise null. Added 2026-09-30 (additive)."
                    }
                  }
                },
                "examples": {
                  "liveKey": {
                    "summary": "Live key mid-window",
                    "value": {
                      "key_type": "live",
                      "hourly_limit": 1000,
                      "used_this_hour": 412,
                      "remaining": 588,
                      "uncapped": false,
                      "resets_at": "2026-07-19T11:00:00.000Z",
                      "total_evaluations": 183204,
                      "limit_hits": 3,
                      "plan": "free",
                      "monthly_limit": 10000,
                      "month_used": 2481,
                      "monthly_lookup_limit": 100000,
                      "month_lookups_used": 5210,
                      "month_resets_at": "2026-08-01T00:00:00.000Z",
                      "grandfathered_until": null
                    }
                  },
                  "uncappedKey": {
                    "summary": "Approved public-interest key mid-window",
                    "value": {
                      "key_type": "live",
                      "hourly_limit": null,
                      "used_this_hour": 412,
                      "remaining": null,
                      "uncapped": true,
                      "resets_at": "2026-09-06T11:00:00.000Z",
                      "total_evaluations": 183204,
                      "limit_hits": 0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/StorageUnavailable"
          },
          "403": {
            "description": "Account suspended. The IP allowlist is not enforced on /v1/usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Account suspended. Contact support@maskbreak.com."
                }
              }
            }
          },
          "429": {
            "description": "The 120 requests/minute/IP usage limiter or shared invalid-key lockout was reached. Applies to capped and uncapped accounts; does not consume evaluation quota.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait: 60 for the usage limiter, 900 for the invalid-key lockout.",
                "schema": {
                  "type": "integer",
                  "enum": [
                    60,
                    900
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "usageLimiter": {
                    "value": {
                      "error": "Too many requests."
                    }
                  },
                  "invalidKeyLockout": {
                    "value": {
                      "error": "Too many invalid API keys from this network. Try again later."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Server error."
                }
              }
            }
          }
        }
      }
    },
    "/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 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"
                        }
                      }
                    },
                    "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 (an IPv6 caller's whole /64 shares 72/minute and 480/day). Pasted forms such as 1.2.3.4:80, [2001:db8::1]:443, a URL, fullwidth digits or ::ffff:1.2.3.4 are read as the address they name, and an IPv6 address is answered in its RFC 5952 form. Invalid input does not count toward those limits (it has its own, 60/minute). 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 (no email required)",
        "description": "Check up to 20 unique public IPs in one call. Does not collect email, subscribe callers or send mail; an email supplied by a legacy client is ignored. Limited to 3 batches/hour per caller IP (an IPv6 /64 shares 12 batches/hour); a batch with no valid public address does not count. Entries are read like /api/lookup and deduplicated after that. IP-only results do not establish that a visitor is safe or provide browser evidence. 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"
                ],
                "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",
                    "deprecated": true,
                    "description": "Legacy property, ignored. Not required, stored or used to send email."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-IP verdicts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer",
                      "description": "IPs evaluated."
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Unique entries omitted because they are invalid, reserved or beyond the 20-IP cap (invalid + not_checked). Duplicates, including two spellings of one address, are removed before this count."
                    },
                    "invalid": {
                      "type": "integer",
                      "description": "Of the skipped entries, those that are not a public IP address (malformed, private or reserved)."
                    },
                    "not_checked": {
                      "type": "integer",
                      "description": "Of the skipped entries, valid public addresses beyond the 20-IP cap (not checked; send them in another batch)."
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LookupResponse"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing IP list or no valid public 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"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "summary": "Exchange an API key for a short-lived bearer token",
        "description": "OAuth 2.0 client_credentials grant. client_id is your account email and client_secret is your current live or personal test API key; a previous live key in its rotation grace period cannot mint tokens. The access_token carries the account id and key type, never the API key. It expires after 3600 seconds. It is bound to the key it was minted with and authenticates as the account's current key of that type while that key still works: after a rotation, a token minted from the previous live key keeps working through that key's 24-hour grace; revoking the previous key, a second rotation, or rotating the test key ends such tokens at once (401), so mint a new token on a 401. Use Bearer authentication on /v1/*; authenticated MCP also accepts tokens minted with live keys. Supply exchange credentials in the form body (client_secret_post) or through HTTP Basic (client_secret_basic). Token responses send no-store. A live key's IP allowlist applies to minting too (since 2026-09-28): from an address outside the list the answer is 400 unauthorized_client and no token; the test key is exempt. Limited to 60 exchanges/minute/IP; a 429 carries Retry-After: 60. Discovery: /.well-known/oauth-authorization-server.",
        "operationId": "oauthToken",
        "tags": [
          "Authentication"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "client_credentials"
                    ],
                    "description": "Must be client_credentials; no other grant is supported."
                  },
                  "client_id": {
                    "type": "string",
                    "format": "email",
                    "description": "Your account email. Omit if using HTTP Basic."
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "Your API key (sk_live_… or sk_test_…). Omit if using HTTP Basic."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A short-lived access token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "JWT bearer token."
                    },
                    "token_type": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Lifetime in seconds."
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "api"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "unsupported_grant_type; invalid_request (client_id/client_secret missing, not strings, or longer than 256 characters); or unauthorized_client: a live key presented from an address outside its IP allowlist (since 2026-09-28; the test key is exempt).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "invalid_client. Identical for a wrong email and a wrong key — the response never says which half failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "503": {
            "description": "The account store could not be read, so the credentials could not be checked (not proof they are wrong). Retry after Retry-After (30 seconds). Added 2026-09-26; before that this case answered 401 invalid_client.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (30).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                },
                "example": {
                  "error": "temporarily_unavailable",
                  "error_description": "Service temporarily unavailable. Please retry.",
                  "code": "storage_unavailable"
                }
              }
            }
          },
          "429": {
            "description": "More than 60 token exchanges/minute/IP. Retry-After says when to retry (60 seconds; returned since 2026-09-28).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (60).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                },
                "example": {
                  "error": "rate_limited",
                  "error_description": "Too many token requests."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_live_...",
        "description": "Pass your Maskbreak API key from https://maskbreak.com/dashboard as a Bearer token. Live keys start with sk_live_. After rotation the previous live key remains valid for 24 hours unless revoked early in Settings. Every account also has a personal sk_test_ key: it runs the complete live pipeline, stores events flagged as test and keeps them out of your stats, never fires webhooks, and is exempt from the IP allowlist; its checks count toward the monthly allowance (the fixed test_* tokens do not). The public sk_test_sandbox key needs no account and works only on POST /v1/evaluate with deterministic test_* tokens."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "Exchange your account email + current live or personal test API key at /oauth/token. Issued tokens expire after 3600 seconds and are bound to the key they were minted with: they keep working while that key does (including the previous live key's 24-hour rotation grace) and stop at once (401) when it is revoked or rotated out.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://maskbreak.com/oauth/token",
            "scopes": {
              "api": "Access to /v1 endpoints under the account's live or test credential. Limits follow the account's plan (Free: 10,000 visitor checks a month, up to 1,000 an hour); approved public-interest keys are uncapped."
            }
          }
        }
      }
    },
    "schemas": {
      "LookupResponse": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string",
            "description": "The IP that was looked up."
          },
          "known": {
            "type": "boolean",
            "description": "Whether an available intelligence source returned data for this IP. false means no source had data at lookup time, which can include unavailable feeds; it does not assert the IP is clean. 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"
                ]
              },
              "cloud": {
                "type": "string",
                "description": "Provider identifier when the IP matches a published cloud range."
              },
              "registration_country": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Country the autonomous system is registered in, from the public routing table. Present with source asn_table. Not the visitor's location; never scored."
              },
              "source": {
                "type": "string",
                "enum": [
                  "asn_table"
                ],
                "description": "Present when asn and registration_country come from the public IP-to-ASN table (display only, never scored)."
              }
            }
          },
          "latency_ms": {
            "type": "integer"
          },
          "engine_verdict": {
            "type": "string",
            "enum": [
              "allow",
              "review",
              "block"
            ],
            "description": "The engine's own verdict, present whenever an IP exception matches, including when the exception keeps the same 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",
        "properties": {
          "token": {
            "type": [
              "string",
              "null"
            ],
            "description": "Needed for full network intelligence: the browser SDK injects this token into the monocle hidden input; forward it as token (monocle is accepted as an alias; token wins when both are sent). With an account credential, omission, null or an empty string is accepted as a degraded HTTP fallback. Official server SDKs currently require a non-empty token. Deterministic test tokens (test_clean, test_vpn, test_proxy, test_datacenter, test_tor) are never billed or webhooked; console-originated calls with a live key are stored as test events. The public sandbox accepts only these test tokens.",
            "example": "eyJhbGciOi..."
          },
          "fingerprintEventId": {
            "type": "string",
            "description": "Optional device-intelligence event id for additional device signals. sentinel_fp is accepted as an alias. Device fields are omitted if the event cannot be retrieved.",
            "example": "fp_event_a8c..."
          },
          "accountId": {
            "type": "string",
            "maxLength": 256,
            "description": "Optional pseudonymous account/user identifier, set by your backend from its own authenticated session, never copied from a browser-claimed ID. Use your own internal ID, never an email address or a name. It powers two things, both scoped to your account only and never linked across customers: device-link counts (device.linked_accounts, with a verified fingerprintEventId) and the account network (the `account` object, since 18 October 2026). Maskbreak keeps a keyed hash of it for linking and an encrypted copy so your console can show which of your accounts are linked; both are deleted 90 days after the account was last checked. Multiple accounts may be legitimate shared-device use, not fraud.",
            "example": "user_8123"
          },
          "gpuEvidence": {"$ref": "#/components/schemas/GpuEvidence"},
          "browserEvidence": {"$ref": "#/components/schemas/BrowserConsistencyEvidence"},
          "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"
          },
          "tz": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional visitor IANA timezone. Sentinel.collect() returns tz; HTML forms receive sentinel_tz, which is accepted as an alias. Forward it to this HTTP endpoint explicitly; the current server SDKs do not accept timezone input. A recognized zone adds timezone fields when the exit IP country is available. A country mismatch adds timezone_mismatch, raises risk_score and escalates allow to review only when the network has no VPN, proxy, Tor or datacenter signal. Never blocks on its own.",
            "example": "Europe/Tallinn"
          }
        }
      },
      "GpuEvidence": {
        "type": "object",
        "description": "Optional explicit WebGPU beta measurement from MaskbreakGPU.collect(challenge). Submit with the SAME fingerprintEventId used for the challenge. Never changes decisions or automatically links devices. Raw HTTP supported; published server SDKs may omit this input.",
        "required": ["challenge", "protocol", "features"],
        "properties": {
          "challenge": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
          "protocol": {"const": "atomic-order-group-median-v1:128x128:32:low-power"},
          "features": {"type": "array", "minItems": 128, "maxItems": 128, "items": {"type": "number", "minimum": 0, "maximum": 1}}
        }
      },
      "BrowserClaim": {
        "type": "object", "required": ["browser", "os"],
        "properties": {
          "browser": {"enum": ["chromium", "firefox", "safari", null]},
          "os": {"enum": ["windows", "macos", "linux", "android", "ios", "chromeos", null]}
        }
      },
      "BrowserConsistencyEvidence": {
        "type": "object",
        "description": "Explicit experimental review-step report from MaskbreakBrowserConsistency.collect(challenge). Requires the existing GPU beta opt-in and SAME fresh fingerprintEventId. Submit either browserEvidence or gpuEvidence per event; browserEvidence takes precedence if both are sent. Raw reports are processed transiently, not persisted. Client values can be forged; this is not hardware attestation or proof of work. Raw HTTP supported; published SDKs may omit the field.",
        "required": ["protocol", "challenge", "page", "worker", "graphics", "computations"],
        "properties": {
          "protocol": {"const": "browser-consistency-v1"},
          "challenge": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
          "page": {"type": "array", "minItems": 2, "maxItems": 2, "items": {"$ref": "#/components/schemas/BrowserClaim"}},
          "worker": {"type": "array", "minItems": 2, "maxItems": 2, "items": {"anyOf": [{"$ref": "#/components/schemas/BrowserClaim"}, {"type": "null"}]}},
          "graphics": {"type": "object", "required": ["webgl", "webgpu", "fallback"], "properties": {
            "webgl": {"enum": ["apple", "intel", "amd", "nvidia", "qualcomm", "arm", "software", null]},
            "webgpu": {"enum": ["apple", "intel", "amd", "nvidia", "qualcomm", "arm", "software", null]},
            "fallback": {"type": "boolean"}
          }},
          "computations": {"type": "array", "minItems": 2, "maxItems": 2, "items": {"anyOf": [
            {"type": "null"}, {"type": "array", "minItems": 32, "maxItems": 32, "items": {"type": "integer", "minimum": 0, "maximum": 4294967295}}
          ]}}
        }
      },
      "BrowserConsistencyResult": {
        "type": "object",
        "description": "Server-recomputed but browser-reported experimental observations: forgeable browser reports, not hardware attestation. Most findings change nothing; differences may reflect extensions or driver bugs, and missing WebGPU or software rendering is unknown. Since 17 October 2026 three findings raise an allow to review (never to block) and set affects_decision: a WebGL report naming a GPU vendor or backend the claimed system cannot have, or two vendors at once (gpu_os_plausibility, gpu_backend_os, gpu_vendor_agreement; reason gpu_inconsistent); one account showing two canvas fingerprints from two browsers on the same machine type (reason antidetect_randomization, needs accountId); and five or more new canvas fingerprints on one machine type within 24 hours, each shown by one browser only (reason antidetect_profile_burst). The report and its summary are not saved in event details; for the last two, keyed hashes of the canvas readings, of the browser's device identifier and of the account id are kept for 30 days, for this customer only.",
        "properties": {
          "status": {"enum": ["consistent", "inconsistent", "inconclusive", "disabled", "invalid", "expired_or_used", "not_saved", "unavailable"]},
          "experimental": {"const": true}, "source": {"const": "browser_reported"},
          "affects_decision": {"type": "boolean", "description": "True when one of the findings above raised this answer from allow to review."},
          "findings": {"type": "array", "items": {"enum": ["browser_context", "gpu_calculation", "gpu_os_plausibility", "gpu_backend_os", "gpu_vendor_agreement"]}},
          "checks": {"type": "array", "items": {"type": "object", "properties": {
            "id": {"enum": ["browser_context", "graphics_paths", "gpu_calculation", "gpu_os_plausibility", "gpu_backend_os", "gpu_vendor_agreement"]},
            "status": {"enum": ["consistent", "mismatch", "different", "unavailable"]},
            "fields": {"type": "array", "items": {"enum": ["browser", "os"]}}
          }}},
          "measurements": {"type": "object", "description": "8-character hashes of three canvases drawn in the browser (the images never leave it): canvas_default, a fingerprint scene on the accelerated path; canvas_cpu, the same scene on the CPU raster path; canvas_hardware, a second pattern on the accelerated path. canvas_acceleration says whether the first two differ.", "properties": {
            "canvas_default": {"type": ["string", "null"]}, "canvas_cpu": {"type": ["string", "null"]}, "canvas_hardware": {"type": ["string", "null"]},
            "canvas_acceleration": {"enum": ["accelerated", "software", "unavailable"]}
          }},
          "hardware_profiles": {"type": "integer", "minimum": 1, "description": "With an accountId and an accelerated canvas: how many canvas fingerprints this account has shown on this machine type and browser version in 30 days. Two from two browsers is antidetect_randomization."},
          "machine_profiles": {"type": "integer", "minimum": 1, "description": "Present when this visit's canvas fingerprint is new on its machine type within 24 hours and no other browser has shown it: how many such fingerprints the machine type has now. Five from five browsers is antidetect_profile_burst."}
        }
      },
      "CustomerDeviceHistory": {
        "type": "object",
        "description": "Distinct fresh provider-verified events for this customer in a rolling 90-day window, beginning when the feature was introduced. Replays count once, including for account links. Test keys excluded. First seen is first in the retained window, not a lifetime first visit. No matching across customers.",
        "properties": {
          "device_key": {"type": "string"},
          "window_days": {"const": 90},
          "visits": {"type": "integer", "minimum": 1},
          "returning": {"type": "boolean"},
          "first_seen": {"type": "string", "format": "date-time"},
          "last_seen": {"type": "string", "format": "date-time"},
          "accounts": {"type": "integer", "minimum": 0}
        }
      },
      "GpuEvidenceResult": {
        "type": "object",
        "description": "Optional beta outcome; never a fraud score or identity assertion. Similar GPUs may resemble each other and measurements are untrusted browser input. Comparison is limited to this customer's latest retained samples (up to 200 devices, 30 days).",
        "properties": {
          "status": {"enum": ["recorded", "disabled", "invalid", "expired_or_used", "not_saved", "unavailable"]},
          "experimental": {"const": true},
          "nearest": {"type": ["object", "null"], "properties": {
            "device_key": {"type": "string"},
            "distance": {"type": "number", "minimum": 0, "description": "RMS feature distance. Lower is closer; no validated identity threshold."}
          }}
        }
      },
      "TimezoneSignal": {
        "type": "object",
        "description": "Present when the request includes a recognized tz (or sentinel_tz) and the exit IP country is available. Unknown zones and missing IP countries omit the block.",
        "properties": {
          "reported": {
            "type": "string",
            "description": "The IANA zone the browser reported, echoed back.",
            "example": "Europe/Tallinn"
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 country that zone belongs to.",
            "example": "EE"
          },
          "matches_ip": {
            "type": "boolean",
            "description": "True when the zone's country equals the exit IP's country."
          },
          "checked": {
            "type": "boolean",
            "description": "False on a VPN, proxy, Tor or datacenter exit, where a mismatch is expected and nothing is raised. When false, read matches_ip as information rather than as a verdict."
          }
        }
      },
      "NetworkSignals": {
        "type": "object",
        "description": "Network-layer signals derived from Maskbreak'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. Also true for Apple's iCloud Private Relay; privacy_relay says when it is that relay."
          },
          "privacy_relay": {
            "type": "boolean",
            "description": "Additive (since 10 October 2026), always present. True when the proxy is Apple's iCloud Private Relay, as Maskbreak's network intelligence recognises it (proxy is true too). On its own it is review, never block: the reason is privacy_relay instead of proxy_detected and it weighs 35 in risk_score, like a VPN. A proxy rule set to allow allows it too; a proxy rule set to review or block does not apply to it, so it stays review."
          },
          "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"
          },
          "complete": {
            "type": "boolean",
            "description": "Additive. True when the network assessment was complete. False for a partial reading (the visitor submitted before the network check finished; a partial reading has a higher chance of a wrong answer) and when there was no assessment at all (then degraded is true). Present on live-pipeline evaluations (live and personal test keys); absent on deterministic test tokens, the sandbox and samples. Informational: it does not change decision."
          },
          "sdk_blocked": {
            "type": "boolean",
            "description": "Additive. True when a token was decoded but the network check could not measure the visitor: the address it reports is private, missing, or the network check's own infrastructure, which is what a blocked or stripped measurement produces (ad blockers, antidetect tooling). The network flags are then not evidence of a clean connection. Present on live-pipeline evaluations, like complete. Informational: it does not change decision or degraded; hold sensitive actions yourself if you need to."
          }
        }
      },
      "DeviceSignals": {
        "type": "object",
        "description": "Device-layer signals returned when fingerprintEventId (or sentinel_fp) resolves successfully. Omitted when device intelligence is unavailable. Check checked_signals before interpreting a false boolean: unchecked fields retain false for compatibility, not as proof of a negative result. The public bot sample includes fixture device signals without a client event id.",
        "properties": {
          "complete": {
            "type": "boolean",
            "description": "True when browser tampering and bot checks returned valid results. Does not mean every optional or mobile-only check ran, or guarantee a genuine browser. Does not change the decision policy."
          },
          "checked_signals": {
            "type": "array",
            "uniqueItems": true,
            "items": { "type": "string", "enum": ["antidetect", "automation", "emulator", "virtual_machine", "incognito", "privacy_mode", "ip_blocklisted", "high_activity"] },
            "description": "Boolean device fields backed by an explicit result. A field absent from this list was not checked successfully, even if its compatibility boolean is false. Older responses and synthetic demo fixtures may omit this metadata; do not infer check availability from that omission."
          },
          "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": "The browser runs inside a virtual machine (VMware, VirtualBox and similar). Since 29 September 2026 this makes the base decision review on its own (never block by itself), like a VPN alone."
          },
          "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": "Opaque visitor identifier returned by the device-intelligence layer. Maskbreak hashes this identifier for stored device history and account links.",
            "example": "v_a8c91b..."
          },
          "tampering_score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Upstream tampering score, 0–1, or 0 when unavailable. Not a probability of fraud or proof of a genuine browser. Use checked_signals and antidetect to distinguish missing checks from an explicit detection result.",
            "example": 0.27
          },
          "customer_history": {"$ref": "#/components/schemas/CustomerDeviceHistory"},
          "gpu_evidence": {"$ref": "#/components/schemas/GpuEvidenceResult"},
          "browser_consistency": {"$ref": "#/components/schemas/BrowserConsistencyResult"},
          "times_seen": {
            "type": "integer",
            "description": "Cumulative sightings in Maskbreak's shared device-history record, keyed by a hash of the visitor id, not by customer or API key. This is not a rolling 90-day count: periodic cleanup removes records inactive for more than 90 days. Present when a device event resolves to a visitor id and history recording succeeds.",
            "example": 7
          },
          "first_seen": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "ISO 8601 start time of the retained shared device-history record, not the first visit under your API key. The record is eligible for periodic deletion after more than 90 days of inactivity; a later sighting after deletion starts new history. Present when device history is available.",
            "example": "2026-05-02T11:26:14.000Z"
          },
          "returning": {
            "type": "boolean",
            "description": "True when times_seen > 1 in the retained shared device history; does not mean the device has previously visited this customer.",
            "example": true
          },
          "linked_accounts": {
            "type": "integer",
            "description": "Distinct retained accountId hashes linked to this device for this customer only, across that customer's keys. Unlike shared device history, account links never span customers. Periodic cleanup removes each link after more than 90 days of inactivity; this count uses the retained links. Present when accountId and a resolved device event are supplied and link recording succeeds.",
            "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
          }
        }
      },
      "AccountNetwork": {
        "type": "object",
        "description": "The account network (since 18 October 2026, additive): present when you send accountId with a live key and the account's records were written within the check's time budget. Never for the test key or the console's own checks. It counts, for your account only, which devices and addresses your accounts were checked from; nothing links across customers. An address is never linked when it is a shared exit (VPN, proxy, datacenter, Tor, Apple's iCloud Private Relay, a mobile carrier's shared address), and an IPv6 address counts by its /64. Records are deleted 90 days after the account was last checked.",
        "properties": {
          "seen_before": {
            "type": "boolean",
            "description": "This accountId was checked before, within the last 90 days."
          },
          "first_seen": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When this accountId was first checked, within the 90-day window."
          },
          "new_device": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "True when the account was checked before on another device and never on this one; adds the reason account_new_device (informational). Null when this check carried no device evidence."
          },
          "devices_24h": {
            "type": "integer",
            "description": "Distinct devices this account was checked on in the last 24 hours. 3 or more adds account_many_devices (informational)."
          },
          "devices_90d": {
            "type": "integer",
            "description": "Distinct devices this account was checked on in the last 90 days."
          },
          "device_accounts_24h": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Your accounts checked on this check's device in the last 24 hours, this one included. 3 or more adds device_many_accounts, which raises allow to review (never to block); a device_sharing rule can relax or tighten it. Null when this check carried no device evidence."
          },
          "ip_accounts_24h": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Your accounts checked from this check's address in the last 24 hours, this one included. 3 or more adds ip_many_accounts (informational). Null when the network check measured no address or the address is a shared exit."
          },
          "connected_accounts": {
            "type": "integer",
            "description": "Your other accounts that share a device or an address with this one within 90 days."
          },
          "connected_by_device": {
            "type": "integer",
            "description": "Of connected_accounts, those that share a device."
          }
        },
        "example": {
          "seen_before": true,
          "first_seen": "2026-10-01T09:12:44.000Z",
          "new_device": false,
          "devices_24h": 1,
          "devices_90d": 2,
          "device_accounts_24h": 3,
          "ip_accounts_24h": 1,
          "connected_accounts": 2,
          "connected_by_device": 2
        }
      },
      "EvaluateResponse": {
        "type": "object",
        "description": "Maskbreak 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": "Legacy engine flag: true for network VPN/proxy/Tor or device browser tampering, automation, emulator or virtual-machine signals (virtual machine since 29 September 2026). Email, timezone, custom rules and exception pins do not change this flag; route on decision for the final action."
          },
          "details": {
            "type": "object",
            "description": "Raw network signals (legacy shape, kept for backwards compatibility). Since 2026-09-28 it no longer carries two undocumented internal identifiers of the network check (an assessment id and an application name); every documented field is unchanged. For the age of the network reading, read evidence.network_age_s.",
            "properties": {
              "ip": {
                "type": "string",
                "example": "198.51.100.18"
              },
              "cc": {
                "type": [
                  "string",
                  "null"
                ],
                "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 device-intelligence signals (legacy shape). Present when a device event resolves successfully, or in the public bot sample fixture.",
            "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": "Final recommended action, including customer rules and exception pins. The base engine blocks on proxy, Tor, browser tampering, automation or emulator signals, reviews VPN-only threats, a browser inside a virtual machine (since 29 September 2026; it was allow before) and Apple's iCloud Private Relay on its own (network.privacy_relay, since 10 October 2026; it was blocked as a proxy before), and otherwise allows. Disposable email, eligible timezone mismatches and three findings of the opt-in browser-consistency beta (gpu_inconsistent, antidetect_randomization, antidetect_profile_burst; since 17 October 2026) can escalate allow to review. A datacenter or anonymous network alone does not change the base allow decision."
          },
          "risk_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Composite risk score across all network and device signals.",
            "example": 71
          },
          "ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "The visitor's address as the network layer measured it. The string \"unknown\" in a degraded answer without a client token.",
            "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"
          },
          "account": {
            "$ref": "#/components/schemas/AccountNetwork"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Machine-readable reason codes for the decision: vpn_detected, proxy_detected, privacy_relay (Apple's iCloud Private Relay, in place of proxy_detected since 10 October 2026), datacenter_asn, tor_exit_node, anonymous_network, antidetect_browser, automation_detected, emulator_detected, virtual_machine, ip_blocklisted, private_browsing, high_activity_device, disposable_email, timezone_mismatch, multi_account_device, and from the opt-in browser-consistency beta (since 17 October 2026; each raises allow to review, never to block) gpu_inconsistent, antidetect_randomization and antidetect_profile_burst, and from the account network (since 18 October 2026, when you send accountId) account_new_device, account_many_devices and ip_many_accounts (informational) and device_many_accounts (3 or more of your accounts on one device within 24 hours; raises allow to review, never to block). New codes are only ever added.",
            "example": [
              "vpn_detected",
              "datacenter_asn"
            ]
          },
          "evaluated_in_ms": {
            "type": "integer",
            "example": 28,
            "description": "Server-side processing time for this request."
          },
          "degraded": {
            "type": "boolean",
            "description": "Present and true when the network layer is unavailable, including a missing or empty client token. A device-provider failure alone does not set this flag; unavailable device fields are omitted. The response still includes available signals and customer policy. A degraded allow result with isSuspicious:false and no device reading (no fingerprintEventId, or one that could not be read) is not counted in total_evaluations or the monthly allowance and is not written to Events, but uses one hourly request. Other degraded results, including a device-only check (device_evidence ok), follow normal storage and usage rules, including test-key exclusions."
          },
          "device_evidence": {
            "type": "string",
            "enum": [
              "ok",
              "unavailable",
              "not_sent"
            ],
            "description": "Additive. Whether the device layer was usable for this call: \"ok\" when the device event resolved (the device object is present), \"unavailable\" when a fingerprintEventId was sent but did not resolve (malformed, expired, from elsewhere, or the device provider was unreachable), \"not_sent\" when no event id was sent (for example a form whose sentinel_fp field was removed). Present on live-pipeline evaluations (live and personal test keys); absent on deterministic test tokens, the sandbox and samples. Informational: it does not change decision. Hold sensitive actions when it is not \"ok\".",
            "example": "ok"
          },
          "evidence": {
            "type": "object",
            "description": "Additive. How fresh the evidence was when this call scored it, with only the fields that are known (absent when none is). Informational: it does not change decision. Evidence minted long before the protected action, or a device event marked replayed, deserves a fresh check.",
            "properties": {
              "network_age_s": {
                "type": "integer",
                "minimum": 0,
                "description": "Seconds between the network assessment and this call. Present when a network token was decoded."
              },
              "device_age_s": {
                "type": "integer",
                "minimum": 0,
                "description": "Seconds between the device event and this call. Present when the device event resolved and carried a time."
              },
              "device_replayed": {
                "type": "boolean",
                "description": "True when the device provider marked the event as a replay of an earlier one. Present when the provider reported it."
              }
            }
          },
          "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\". A rule decides only for the signal it names: fired signals no rule covers keep the engine's default weight, so a rule never lowers a block caused by another signal. Signals without a rule of their own (an emulator, a virtual machine, a timezone mismatch) always keep that weight. So does Apple's iCloud Private Relay (network.privacy_relay, since 10 October 2026) unless a proxy rule set to allow covers it: a proxy rule set to review or block does not apply to the relay. The account network's signals have rules of their own (since 18 October 2026): device_sharing (device_many_accounts, reviewed by default), account_sharing (account_many_devices), new_device (account_new_device) and shared_ip (ip_many_accounts); the last three change a decision only through their rule."
          },
          "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 email is a string of 3-254 characters containing @: whether the address uses a disposable/burner domain. Other inputs omit this block."
          },
          "timezone": {
            "$ref": "#/components/schemas/TimezoneSignal"
          },
          "test": {
            "type": "boolean",
            "description": "True for deterministic test tokens and personal test-key responses. Neither counts toward total_evaluations or fires webhooks. Live-pipeline test-key calls store test events unless they are silent degraded results; deterministic tokens store events only for console-originated live-key calls. The public sandbox never stores events."
          },
          "sandbox": {
            "type": "boolean",
            "description": "True when POST /v1/evaluate used the public sk_test_sandbox key."
          },
          "sample": {
            "type": "boolean",
            "description": "True on GET /v1/evaluate/sample fixture responses."
          }
        }
      },
      "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"
          },
          "code": {
            "type": "string",
            "description": "Optional, additive: a stable machine-readable reason where one exists. storage_unavailable (with HTTP 503 and Retry-After) means the key or account could not be checked right now; retry, it is not an invalid key. monthly_quota_exceeded (with HTTP 429, since 2026-09-30) means the calendar month's allowance is used up; Retry-After counts down to 00:00 UTC on the 1st. Branch on code, not on the error text.",
            "example": "storage_unavailable"
          }
        }
      },
      "WebhookThreatEvent": {
        "type": "object",
        "description": "Delivered to your configured HTTPS endpoint on engine threats and rule/exception blocks. Signed: X-Maskbreak-Signature = 'sha256=' + HMAC_SHA256(secret, X-Maskbreak-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-Maskbreak-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, privacy_relay, tor, dch, bot, tampering, antidetect, virtual_machine), decision, and rule_matched/exception_matched attribution. tampering is browser tampering or an emulator; virtual_machine (additive since 29 September 2026, when a browser inside a virtual machine started to count as a threat that asks for review) is its own field, and a visit whose only threat is the virtual machine arrives with threat_type 'Virtual machine'. privacy_relay (additive since 10 October 2026) is true for Apple's iCloud Private Relay, which asks for review: proxied stays true for it, and such a visit arrives with threat_type 'Privacy relay' (unless a VPN flag, a rule or an exception is named first; a relay visit that was blocked anyway is labelled by the block's cause: 'Tor', 'Bot', 'Antidetect browser' or 'Tampering'), never 'Proxy'."
          }
        },
        "required": [
          "event",
          "event_id",
          "timestamp",
          "ip",
          "threat_type"
        ]
      },
      "OAuthError": {
        "type": "object",
        "description": "OAuth token endpoint error. rate_limited is the endpoint's additional 429 error code; temporarily_unavailable (RFC 6749) comes with HTTP 503 and code storage_unavailable when the credentials could not be checked; unauthorized_client (RFC 6749, additive since 2026-09-28) comes with HTTP 400 when a live key is presented from an address outside its IP allowlist.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "unsupported_grant_type",
              "rate_limited",
              "temporarily_unavailable",
              "unauthorized_client"
            ]
          },
          "error_description": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Optional, additive: storage_unavailable on the 503.",
            "example": "storage_unavailable"
          }
        }
      }
    },
    "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. An empty key (`Authorization: Bearer` with nothing after it, which is what an empty key variable sends) answers `Missing Key.`; the hint field names the likely cause.",
        "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"
            }
          }
        }
      },
      "StorageUnavailable": {
        "description": "Service temporarily unavailable: Maskbreak's account store could not be read, so the key or account could not be checked. This is not an invalid key (a key that does not match any account is always 401). Nothing was evaluated and no quota was used. Retry after Retry-After, and apply your outage policy meanwhile; never treat it as an allow verdict. Added 2026-09-26 (additive); before that a storage failure was answered 401 \"Invalid API Key.\".",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying (30).",
            "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"
            },
            "example": {
              "error": "Service temporarily unavailable. Please retry.",
              "code": "storage_unavailable"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests; see the endpoint for its limits. /v1/evaluate and /v1/lookup share the plan's hourly per-account bucket (Free 1,000, Starter 5,000, Growth 20,000, Scale 50,000 an hour; the live key, and the previous key during its rotation grace, count against it; a test key has its own) and a 200,000/hour source-IP backstop. Since 2026-09-30 each plan also has calendar-month allowances (UTC): visitor checks for /v1/evaluate and 10 times that for /v1/lookup. When one is used up the answer is 429 with code monthly_quota_exceeded and a Retry-After of the seconds until 00:00 UTC on the 1st; there are no automatic overage charges. Accounts created before 2026-10-01 keep 1,000 checks an hour with no monthly cap until 30 November 2026. Approved public-interest keys have no hourly cap and bypass the source-IP backstop once recognized. Other per-IP limiters, including the invalid-key lockout, still apply. The public evaluation sandbox has its own 60/minute/IP limit and the sample endpoint has a 30/minute/IP limit.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Hourly request limit for this key. Present when the account 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 the account quota produced the 429.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds when the window resets. Present when the account 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"
            },
            "examples": {
              "monthlyAllowanceUsedUp": {
                "summary": "The calendar month's allowance is used up (added 2026-09-30; error text abbreviated, branch on code)",
                "value": {
                  "error": "…",
                  "code": "monthly_quota_exceeded"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "threatDetected": {
      "post": {
        "summary": "Threat alert pushed to your endpoint",
        "description": "Configured under Alerts on the dashboard Integration tab. 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."
          }
        }
      }
    }
  }
}
