{
  "openapi": "3.1.0",
  "info": {
    "title": "InlineShield API",
    "version": "1.0.0",
    "description": "The InlineShield API screens sign-ups for bonus abuse, fake accounts and bots, and gives one of three verdicts: ALLOWED, QUARANTINED (hold for review) or BLOCKED.\n\nThere are two ways to integrate:\n\n- **Server only:** your backend calls `POST /api/v1/verify` with the player's email, IP and User-Agent.\n- **Browser script and backend:** `inlineshield.js` on your sign-up page calls `POST /api/v1/check` with your site key and gets a single-use token; your backend sends that token to `POST /api/v1/siteverify` with its secret key and gets the decision.\n\nA held sign-up (`action: \"hold\"`) carries a `reviewId`. Poll `GET /api/v1/reviews/{reviewId}` or wait for the `signup.approved` / `signup.rejected` webhook. Report real outcomes with `POST /api/v1/feedback`.\n\n## Authentication\n\n- **Secret API key** (`bs_live_…` live, `bs_test_…` test), sent in the `X-Api-Key` header, for `/verify`, `/siteverify`, `/feedback` and `/reviews/{reviewId}`. Keep it on your server. Create keys in the console under Settings › Keys.\n- **Site key** (`bs_site_…` live, `bs_site_test_…` test), sent in the `siteKey` field of the `/check` body. It is public. A check is accepted only from a browser `Origin` on the site key's list of domains.\n\n## Test mode\n\nA test API key or a test site key puts the request in test mode. Test requests are screened and recorded (History shows them with \"Show test data\"), but they never count against your monthly quota and never create a review, a webhook, a label or alert data. Test traffic has its own per-minute limits, separate from live traffic: 300 per minute per account on each of `/verify` and `/check` (`/check` is also limited to 30 per minute per client IP), and 300 per minute per test key on `/siteverify`. On top of that, 10,000 test screenings (`/verify` and `/check`) per account per UTC day (then `429 TEST_LIMIT`).\n\nIn test mode these five addresses always give the same result without running the engine (the part before @ is case-insensitive; the domain must be exactly `test.inlineshield.io`):\n\n| Address | decision | action | recommendedAction | riskScore |\n|---|---|---|---|---|\n| allow@test.inlineshield.io | ALLOWED | allow | allow | 0 |\n| nobonus@test.inlineshield.io | ALLOWED | allow | allow_no_bonus | 30 |\n| review@test.inlineshield.io | QUARANTINED | hold | review | 50 |\n| kyc@test.inlineshield.io | QUARANTINED | hold | require_kyc | 60 |\n| block@test.inlineshield.io | BLOCKED | block | block | 95 |\n\nA live key screens these addresses like any other. A held test screening has no `reviewId`: test mode creates no review, so the `signup.*` webhooks cannot be triggered with test traffic. Use the console's \"Send test ping\" for a signed `test.ping` delivery.\n\n## Quota and rate limits\n\nEach live `/verify` or `/check` screening counts against the monthly quota of your plan. Over the quota (after a grace margin) both answer `402 QUOTA_EXCEEDED` until the next UTC month; `/siteverify` then answers `{ success: false, error: \"QUOTA_EXCEEDED\" }` for a missing token. Live responses carry `X-Quota-Limit`, `X-Quota-Used`, `X-Quota-Reset` and `X-Quota-State`.\n\nPer-minute limits answer `429 RATE_LIMIT_EXCEEDED` with `Retry-After` (seconds). `/verify`, `/siteverify` and `/feedback` also send `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until the window resets); `/check` and `/reviews/{reviewId}` send only `Retry-After`, on a 429. Browser code can read the quota and rate-limit headers and `Retry-After` (they are listed in `Access-Control-Expose-Headers`).\n\n## Webhooks\n\nInlineShield sends `registration.allowed`, `registration.blocked`, `signup.approved`, `signup.rejected` and `test.ping` (see `webhooks`). A held sign-up arrives as `registration.blocked` with `data.decision: \"QUARANTINED\"`, and is resolved later by `signup.approved` or `signup.rejected`. The event names `incident.created` and `registration.review_required` can appear in an endpoint's subscription list (`incident.created` is there by default), but they are never sent: no delivery ever carries either name.\n\n## Errors\n\nErrors are JSON: `{ \"error\": \"CODE\", \"message\": \"…\" }`. Branch on `error`, not on `message`. A body that is not valid JSON gets `400 BAD_REQUEST`; a body over 1 MB gets `413 PAYLOAD_TOO_LARGE`.\n\n`/siteverify` answers token problems with HTTP 200 and `{ \"success\": false, \"error\": \"…\" }`.",
    "contact": {
      "name": "InlineShield",
      "url": "https://inlineshield.io"
    }
  },
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "servers": [
    {
      "url": "https://api.inlineshield.io",
      "description": "Production and test mode (the key decides which)."
    }
  ],
  "tags": [
    {
      "name": "Screening",
      "description": "Screen a sign-up from your backend."
    },
    {
      "name": "Browser check",
      "description": "The browser script and the backend token check."
    },
    {
      "name": "Reviews",
      "description": "Held sign-ups."
    },
    {
      "name": "Feedback",
      "description": "Report what a player turned out to be."
    },
    {
      "name": "Health",
      "description": "Service status."
    },
    {
      "name": "Webhooks",
      "description": "Events InlineShield sends to your endpoint."
    }
  ],
  "paths": {
    "/api/v1/verify": {
      "post": {
        "operationId": "verifySignup",
        "tags": [
          "Screening"
        ],
        "summary": "Screen a sign-up",
        "description": "Screens one sign-up and returns the verdict, the action to take and the reasons. Call it from your backend before you create the account.\n\n- `action`: `allow` (create the account), `hold` (create it but keep it on hold until the review is decided), `block` (refuse it).\n- `recommendedAction`: advice next to `action`, never instead of it. ALLOWED with `riskScore` 25 or more gives `allow_no_bonus`; QUARANTINED gives `review`; BLOCKED gives `block`.\n- A live hold has a `reviewId`; a test hold has none.\n\nWith the header `X-Response-Mode: http-status` (or `?mode=http-status`) a BLOCKED screening is answered with HTTP 403 and the same body.\n\n`POST /api/v1/evaluate` is a deprecated alias with identical behaviour; use `/api/v1/verify`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "X-Response-Mode",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "http-status"
              ]
            },
            "description": "Answer a BLOCKED screening with HTTP 403 instead of 200."
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "http-status"
              ]
            },
            "description": "Same as the X-Response-Mode header."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              },
              "examples": {
                "allow": {
                  "summary": "Magic address: ALLOWED, allow",
                  "value": {
                    "email": "allow@test.inlineshield.io",
                    "ip": "203.0.113.24",
                    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
                    "clientDwellMs": 8200
                  }
                },
                "nobonus": {
                  "summary": "Magic address: ALLOWED, allow_no_bonus",
                  "value": {
                    "email": "nobonus@test.inlineshield.io",
                    "ip": "203.0.113.24",
                    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
                    "clientDwellMs": 8200
                  }
                },
                "review": {
                  "summary": "Magic address: QUARANTINED, review (test mode: no reviewId)",
                  "value": {
                    "email": "review@test.inlineshield.io",
                    "ip": "203.0.113.24",
                    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
                    "clientDwellMs": 8200
                  }
                },
                "kyc": {
                  "summary": "Magic address: QUARANTINED, require_kyc (test mode: no reviewId)",
                  "value": {
                    "email": "kyc@test.inlineshield.io",
                    "ip": "203.0.113.24",
                    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
                    "clientDwellMs": 8200
                  }
                },
                "block": {
                  "summary": "Magic address: BLOCKED, block",
                  "value": {
                    "email": "block@test.inlineshield.io",
                    "ip": "203.0.113.24",
                    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
                    "clientDwellMs": 8200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The screening result. A live hold also has `reviewId`. Live responses carry the quota headers; test responses do not.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/X-Quota-Used"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-State": {
                "$ref": "#/components/headers/X-Quota-State"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResponse"
                },
                "examples": {
                  "allow": {
                    "summary": "Magic address: ALLOWED, allow",
                    "value": {
                      "requestId": "AUD-MHH3K2QZ-4F1C9B2E7D10",
                      "decision": "ALLOWED",
                      "action": "allow",
                      "recommendedAction": "allow",
                      "verdict": "ALLOWED",
                      "riskScore": 0,
                      "reasons": [
                        "Test mode: allow@test.inlineshield.io always gives ALLOWED / allow."
                      ],
                      "triggers": [
                        "TEST_MAGIC_ADDRESS"
                      ],
                      "policyVersion": "1",
                      "processingTimeMs": 0,
                      "forwardedToDatabase": true,
                      "emailAnalysis": {
                        "normalizedEmail": "allow@test.inlineshield.io",
                        "localPart": "allow",
                        "domain": "test.inlineshield.io",
                        "isDisposable": false,
                        "isPlusAliased": false,
                        "isRoleAccount": false,
                        "hasTypoRisk": false,
                        "mxValid": false,
                        "mx": {
                          "status": "unknown",
                          "records": []
                        },
                        "mxRecords": [],
                        "mailServers": "unknown",
                        "spf": {
                          "exists": false,
                          "status": "unknown",
                          "qualifier": "NONE",
                          "isPermissive": false,
                          "mechanisms": []
                        },
                        "dmarc": {
                          "exists": false,
                          "status": "unknown",
                          "policy": "missing",
                          "compliant": false
                        },
                        "dkim": {
                          "selectorChecked": "",
                          "exists": false,
                          "status": "UNVERIFIABLE"
                        },
                        "localPartRandomness": null
                      },
                      "botAnalysis": {
                        "dwellTimeMs": 8200,
                        "isFastSubmission": false,
                        "honeypotTriggered": false,
                        "isHeadlessBrowser": false,
                        "isAutomationAgent": false,
                        "scriptedUserAgent": false,
                        "scriptedClient": null,
                        "entropyScore": 0
                      },
                      "ipRisk": {
                        "ip": "203.0.113.24",
                        "isDatacenter": null,
                        "isTorExit": null,
                        "isVpnOrProxy": false,
                        "isVpn": null,
                        "country": "Unknown",
                        "countryCode": null,
                        "org": "Unknown",
                        "asn": null
                      }
                    }
                  },
                  "nobonus": {
                    "summary": "Magic address: ALLOWED, allow_no_bonus",
                    "value": {
                      "requestId": "AUD-MHH3K30A-91B7C2D40E55",
                      "decision": "ALLOWED",
                      "action": "allow",
                      "recommendedAction": "allow_no_bonus",
                      "verdict": "ALLOWED",
                      "riskScore": 30,
                      "reasons": [
                        "Test mode: nobonus@test.inlineshield.io always gives ALLOWED / allow_no_bonus."
                      ],
                      "triggers": [
                        "TEST_MAGIC_ADDRESS"
                      ],
                      "policyVersion": "1",
                      "processingTimeMs": 0,
                      "forwardedToDatabase": true,
                      "emailAnalysis": {
                        "normalizedEmail": "nobonus@test.inlineshield.io",
                        "localPart": "nobonus",
                        "domain": "test.inlineshield.io",
                        "isDisposable": false,
                        "isPlusAliased": false,
                        "isRoleAccount": false,
                        "hasTypoRisk": false,
                        "mxValid": false,
                        "mx": {
                          "status": "unknown",
                          "records": []
                        },
                        "mxRecords": [],
                        "mailServers": "unknown",
                        "spf": {
                          "exists": false,
                          "status": "unknown",
                          "qualifier": "NONE",
                          "isPermissive": false,
                          "mechanisms": []
                        },
                        "dmarc": {
                          "exists": false,
                          "status": "unknown",
                          "policy": "missing",
                          "compliant": false
                        },
                        "dkim": {
                          "selectorChecked": "",
                          "exists": false,
                          "status": "UNVERIFIABLE"
                        },
                        "localPartRandomness": null
                      },
                      "botAnalysis": {
                        "dwellTimeMs": 8200,
                        "isFastSubmission": false,
                        "honeypotTriggered": false,
                        "isHeadlessBrowser": false,
                        "isAutomationAgent": false,
                        "scriptedUserAgent": false,
                        "scriptedClient": null,
                        "entropyScore": 0
                      },
                      "ipRisk": {
                        "ip": "203.0.113.24",
                        "isDatacenter": null,
                        "isTorExit": null,
                        "isVpnOrProxy": false,
                        "isVpn": null,
                        "country": "Unknown",
                        "countryCode": null,
                        "org": "Unknown",
                        "asn": null
                      }
                    }
                  },
                  "review": {
                    "summary": "Magic address: QUARANTINED, review (test mode: no reviewId)",
                    "value": {
                      "requestId": "AUD-MHH3K31C-0C3D5E7F9A21",
                      "decision": "QUARANTINED",
                      "action": "hold",
                      "recommendedAction": "review",
                      "verdict": "QUARANTINED",
                      "riskScore": 50,
                      "reasons": [
                        "Test mode: review@test.inlineshield.io always gives QUARANTINED / review."
                      ],
                      "triggers": [
                        "TEST_MAGIC_ADDRESS"
                      ],
                      "policyVersion": "1",
                      "processingTimeMs": 0,
                      "forwardedToDatabase": false,
                      "emailAnalysis": {
                        "normalizedEmail": "review@test.inlineshield.io",
                        "localPart": "review",
                        "domain": "test.inlineshield.io",
                        "isDisposable": false,
                        "isPlusAliased": false,
                        "isRoleAccount": false,
                        "hasTypoRisk": false,
                        "mxValid": false,
                        "mx": {
                          "status": "unknown",
                          "records": []
                        },
                        "mxRecords": [],
                        "mailServers": "unknown",
                        "spf": {
                          "exists": false,
                          "status": "unknown",
                          "qualifier": "NONE",
                          "isPermissive": false,
                          "mechanisms": []
                        },
                        "dmarc": {
                          "exists": false,
                          "status": "unknown",
                          "policy": "missing",
                          "compliant": false
                        },
                        "dkim": {
                          "selectorChecked": "",
                          "exists": false,
                          "status": "UNVERIFIABLE"
                        },
                        "localPartRandomness": null
                      },
                      "botAnalysis": {
                        "dwellTimeMs": 8200,
                        "isFastSubmission": false,
                        "honeypotTriggered": false,
                        "isHeadlessBrowser": false,
                        "isAutomationAgent": false,
                        "scriptedUserAgent": false,
                        "scriptedClient": null,
                        "entropyScore": 0
                      },
                      "ipRisk": {
                        "ip": "203.0.113.24",
                        "isDatacenter": null,
                        "isTorExit": null,
                        "isVpnOrProxy": false,
                        "isVpn": null,
                        "country": "Unknown",
                        "countryCode": null,
                        "org": "Unknown",
                        "asn": null
                      }
                    }
                  },
                  "kyc": {
                    "summary": "Magic address: QUARANTINED, require_kyc (test mode: no reviewId)",
                    "value": {
                      "requestId": "AUD-MHH3K32F-7E2A4C6B8D90",
                      "decision": "QUARANTINED",
                      "action": "hold",
                      "recommendedAction": "require_kyc",
                      "verdict": "QUARANTINED",
                      "riskScore": 60,
                      "reasons": [
                        "Test mode: kyc@test.inlineshield.io always gives QUARANTINED / require_kyc."
                      ],
                      "triggers": [
                        "TEST_MAGIC_ADDRESS"
                      ],
                      "policyVersion": "1",
                      "processingTimeMs": 0,
                      "forwardedToDatabase": false,
                      "emailAnalysis": {
                        "normalizedEmail": "kyc@test.inlineshield.io",
                        "localPart": "kyc",
                        "domain": "test.inlineshield.io",
                        "isDisposable": false,
                        "isPlusAliased": false,
                        "isRoleAccount": false,
                        "hasTypoRisk": false,
                        "mxValid": false,
                        "mx": {
                          "status": "unknown",
                          "records": []
                        },
                        "mxRecords": [],
                        "mailServers": "unknown",
                        "spf": {
                          "exists": false,
                          "status": "unknown",
                          "qualifier": "NONE",
                          "isPermissive": false,
                          "mechanisms": []
                        },
                        "dmarc": {
                          "exists": false,
                          "status": "unknown",
                          "policy": "missing",
                          "compliant": false
                        },
                        "dkim": {
                          "selectorChecked": "",
                          "exists": false,
                          "status": "UNVERIFIABLE"
                        },
                        "localPartRandomness": null
                      },
                      "botAnalysis": {
                        "dwellTimeMs": 8200,
                        "isFastSubmission": false,
                        "honeypotTriggered": false,
                        "isHeadlessBrowser": false,
                        "isAutomationAgent": false,
                        "scriptedUserAgent": false,
                        "scriptedClient": null,
                        "entropyScore": 0
                      },
                      "ipRisk": {
                        "ip": "203.0.113.24",
                        "isDatacenter": null,
                        "isTorExit": null,
                        "isVpnOrProxy": false,
                        "isVpn": null,
                        "country": "Unknown",
                        "countryCode": null,
                        "org": "Unknown",
                        "asn": null
                      }
                    }
                  },
                  "block": {
                    "summary": "Magic address: BLOCKED, block",
                    "value": {
                      "requestId": "AUD-MHH3K33H-5B9D1F3A7C02",
                      "decision": "BLOCKED",
                      "action": "block",
                      "recommendedAction": "block",
                      "verdict": "BLOCKED",
                      "riskScore": 95,
                      "reasons": [
                        "Test mode: block@test.inlineshield.io always gives BLOCKED / block."
                      ],
                      "triggers": [
                        "TEST_MAGIC_ADDRESS"
                      ],
                      "policyVersion": "1",
                      "processingTimeMs": 0,
                      "forwardedToDatabase": false,
                      "emailAnalysis": {
                        "normalizedEmail": "block@test.inlineshield.io",
                        "localPart": "block",
                        "domain": "test.inlineshield.io",
                        "isDisposable": false,
                        "isPlusAliased": false,
                        "isRoleAccount": false,
                        "hasTypoRisk": false,
                        "mxValid": false,
                        "mx": {
                          "status": "unknown",
                          "records": []
                        },
                        "mxRecords": [],
                        "mailServers": "unknown",
                        "spf": {
                          "exists": false,
                          "status": "unknown",
                          "qualifier": "NONE",
                          "isPermissive": false,
                          "mechanisms": []
                        },
                        "dmarc": {
                          "exists": false,
                          "status": "unknown",
                          "policy": "missing",
                          "compliant": false
                        },
                        "dkim": {
                          "selectorChecked": "",
                          "exists": false,
                          "status": "UNVERIFIABLE"
                        },
                        "localPartRandomness": null
                      },
                      "botAnalysis": {
                        "dwellTimeMs": 8200,
                        "isFastSubmission": false,
                        "honeypotTriggered": false,
                        "isHeadlessBrowser": false,
                        "isAutomationAgent": false,
                        "scriptedUserAgent": false,
                        "scriptedClient": null,
                        "entropyScore": 0
                      },
                      "ipRisk": {
                        "ip": "203.0.113.24",
                        "isDatacenter": null,
                        "isTorExit": null,
                        "isVpnOrProxy": false,
                        "isVpn": null,
                        "country": "Unknown",
                        "countryCode": null,
                        "org": "Unknown",
                        "asn": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`email` missing, empty or not a string; an `ip` that is not an IP address; or a body that is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "BAD_REQUEST",
                        "INVALID_IP"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "missingEmail": {
                    "value": {
                      "error": "BAD_REQUEST",
                      "message": "Field \"email\" is required for registration verification."
                    }
                  },
                  "invalidIp": {
                    "value": {
                      "error": "INVALID_IP",
                      "message": "ip must be a valid IPv4 or IPv6 address."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "The monthly quota is used up. Test requests of an account over its quota get this too (they are still not counted).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotaExceeded"
                },
                "examples": {
                  "quota": {
                    "value": {
                      "error": "QUOTA_EXCEEDED",
                      "message": "Monthly check limit reached for this account. Checks resume at 2026-12-01T00:00:00.000Z.",
                      "used": 27500,
                      "limit": 25000,
                      "resetsAt": "2026-12-01T00:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Either a BLOCKED screening with `X-Response-Mode: http-status` (the body is the screening result), or `TENANT_SUSPENDED`.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current one-minute window. Only on a BLOCKED screening in http-status mode (not on TENANT_SUSPENDED).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Only on a BLOCKED screening in http-status mode (not on TENANT_SUSPENDED).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-RateLimit-Reset": {
                "description": "Seconds until the current window ends. Only on a BLOCKED screening in http-status mode (not on TENANT_SUSPENDED).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/X-Quota-Used"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-State": {
                "$ref": "#/components/headers/X-Quota-State"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/VerifyResponse"
                    },
                    {
                      "$ref": "#/components/schemas/SuspendedError"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "`RATE_LIMIT_EXCEEDED`: the per-minute limit (your plan's; 300 for test keys). `TEST_LIMIT`: 10,000 test screenings (/verify and /check) per account per UTC day; `Retry-After` is the time to the next UTC midnight.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "RATE_LIMIT_EXCEEDED",
                        "TEST_LIMIT"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "rateLimit": {
                    "value": {
                      "error": "RATE_LIMIT_EXCEEDED",
                      "message": "Tenant verification rate limit of 600 req/min exceeded. Try again in 12 seconds."
                    }
                  },
                  "testLimit": {
                    "value": {
                      "error": "TEST_LIMIT",
                      "message": "Test mode is limited to 10,000 test screenings (/verify and /check) per account per UTC day. Use a live key for production traffic."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "The screening failed or could not be stored. Your integration decides whether to fail open or closed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "VERIFICATION_ERROR",
                        "PERSISTENCE_FAILURE",
                        "AUTH_ERROR"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/api/v1/check": {
      "post": {
        "operationId": "browserCheck",
        "tags": [
          "Browser check"
        ],
        "summary": "Browser check (site key)",
        "description": "Called by `inlineshield.js` in the player's browser. Authenticated by the site key in the body and the browser's `Origin`, which must be one of the site key's domains. Screens the sign-up and returns the verdict and a single-use `token` (valid for 10 minutes) for `/siteverify`.\n\nThe IP and User-Agent are taken from the request itself, never from the body. Reasons produced by learned signals are shown as one generic line here; `/siteverify` returns them in full.\n\nPer-minute limits: 30 checks per client IP, and your plan's limit per account (300 for test site keys).",
        "security": [],
        "parameters": [
          {
            "name": "Origin",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Set by the browser. Must be an https or http origin on the site key's list of domains."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckRequest"
              },
              "examples": {
                "allow": {
                  "summary": "Magic address: ALLOWED, allow",
                  "value": {
                    "siteKey": "bs_site_test_9QpX2mV7cN4bL1kJ8hG5fD3sA6zW0yT",
                    "email": "allow@test.inlineshield.io",
                    "clientDwellMs": 8200
                  }
                },
                "nobonus": {
                  "summary": "Magic address: ALLOWED, allow_no_bonus",
                  "value": {
                    "siteKey": "bs_site_test_9QpX2mV7cN4bL1kJ8hG5fD3sA6zW0yT",
                    "email": "nobonus@test.inlineshield.io",
                    "clientDwellMs": 8200
                  }
                },
                "review": {
                  "summary": "Magic address: QUARANTINED, review",
                  "value": {
                    "siteKey": "bs_site_test_9QpX2mV7cN4bL1kJ8hG5fD3sA6zW0yT",
                    "email": "review@test.inlineshield.io",
                    "clientDwellMs": 8200
                  }
                },
                "kyc": {
                  "summary": "Magic address: QUARANTINED, require_kyc",
                  "value": {
                    "siteKey": "bs_site_test_9QpX2mV7cN4bL1kJ8hG5fD3sA6zW0yT",
                    "email": "kyc@test.inlineshield.io",
                    "clientDwellMs": 8200
                  }
                },
                "block": {
                  "summary": "Magic address: BLOCKED, block",
                  "value": {
                    "siteKey": "bs_site_test_9QpX2mV7cN4bL1kJ8hG5fD3sA6zW0yT",
                    "email": "block@test.inlineshield.io",
                    "clientDwellMs": 8200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict and a token for `/siteverify`. Live responses carry the quota headers; test responses do not.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/X-Quota-Used"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-State": {
                "$ref": "#/components/headers/X-Quota-State"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResponse"
                },
                "examples": {
                  "allow": {
                    "summary": "Magic address: ALLOWED, allow",
                    "value": {
                      "decision": "ALLOWED",
                      "riskScore": 0,
                      "reasons": [
                        "Test mode: allow@test.inlineshield.io always gives ALLOWED / allow."
                      ],
                      "requestId": "AUD-MHH3K2QZ-4F1C9B2E7D10",
                      "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i"
                    }
                  },
                  "nobonus": {
                    "summary": "Magic address: ALLOWED, allow_no_bonus",
                    "value": {
                      "decision": "ALLOWED",
                      "riskScore": 30,
                      "reasons": [
                        "Test mode: nobonus@test.inlineshield.io always gives ALLOWED / allow_no_bonus."
                      ],
                      "requestId": "AUD-MHH3K30A-91B7C2D40E55",
                      "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i"
                    }
                  },
                  "review": {
                    "summary": "Magic address: QUARANTINED, review",
                    "value": {
                      "decision": "QUARANTINED",
                      "riskScore": 50,
                      "reasons": [
                        "Test mode: review@test.inlineshield.io always gives QUARANTINED / review."
                      ],
                      "requestId": "AUD-MHH3K31C-0C3D5E7F9A21",
                      "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i"
                    }
                  },
                  "kyc": {
                    "summary": "Magic address: QUARANTINED, require_kyc",
                    "value": {
                      "decision": "QUARANTINED",
                      "riskScore": 60,
                      "reasons": [
                        "Test mode: kyc@test.inlineshield.io always gives QUARANTINED / require_kyc."
                      ],
                      "requestId": "AUD-MHH3K32F-7E2A4C6B8D90",
                      "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i"
                    }
                  },
                  "block": {
                    "summary": "Magic address: BLOCKED, block",
                    "value": {
                      "decision": "BLOCKED",
                      "riskScore": 95,
                      "reasons": [
                        "Test mode: block@test.inlineshield.io always gives BLOCKED / block."
                      ],
                      "requestId": "AUD-MHH3K33H-5B9D1F3A7C02",
                      "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No `email`, or a body that is not valid JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "BAD_REQUEST"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "No site key, or an unknown or revoked one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "INVALID_SITE_KEY"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "invalid": {
                    "value": {
                      "error": "INVALID_SITE_KEY",
                      "message": "Unknown or revoked site key."
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The monthly quota is used up (no token is issued).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotaExceeded"
                }
              }
            }
          },
          "403": {
            "description": "The `Origin` is missing or not one of the site key's domains.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "ORIGIN_NOT_ALLOWED"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "origin": {
                    "value": {
                      "error": "ORIGIN_NOT_ALLOWED",
                      "message": "Requests from evil.test are not allowed for this site key. Add evil.test to the site's domains."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "A per-minute limit, or (test site key) the daily test limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "RATE_LIMIT_EXCEEDED",
                        "TEST_LIMIT"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "The check failed. `inlineshield.js` then lets the form submit (fail open).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "VERIFICATION_ERROR",
                        "PERSISTENCE_FAILURE"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/siteverify": {
      "post": {
        "operationId": "siteVerify",
        "tags": [
          "Browser check"
        ],
        "summary": "Redeem a check token",
        "description": "Your backend sends the token that `inlineshield.js` put in the form, with the email the player submitted, and gets the decision recorded by `/check`. A token works once, for 10 minutes, for the same email (compared without case or surrounding spaces), and only with a secret key of the same account.\n\nToken problems are answered with HTTP 200 and `success: false`. `QUOTA_EXCEEDED` means the token is missing because the account is over its quota (`/check` issued none). Nothing here counts against the quota.\n\nLimited per API key to the key's own per-minute limit (1,200 for a live key, 300 for a test key, as created).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SiteverifyRequest"
              },
              "examples": {
                "allow": {
                  "summary": "Token of a check of allow@test.inlineshield.io",
                  "value": {
                    "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i",
                    "email": "allow@test.inlineshield.io"
                  }
                },
                "kyc": {
                  "summary": "Token of a check of kyc@test.inlineshield.io (a test hold: no reviewId)",
                  "value": {
                    "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i",
                    "email": "kyc@test.inlineshield.io"
                  }
                },
                "noToken": {
                  "summary": "The form arrived without a token",
                  "value": {
                    "email": "allow@test.inlineshield.io"
                  }
                },
                "invalidToken": {
                  "summary": "A token this account never issued",
                  "value": {
                    "token": "not-a-real-token",
                    "email": "allow@test.inlineshield.io"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`success: true` with the decision (a live hold also has `reviewId`), or `success: false` with the reason.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteverifyResponse"
                },
                "examples": {
                  "allow": {
                    "value": {
                      "success": true,
                      "decision": "ALLOWED",
                      "riskScore": 0,
                      "reasons": [
                        "Test mode: allow@test.inlineshield.io always gives ALLOWED / allow."
                      ],
                      "requestId": "AUD-MHH3K2QZ-4F1C9B2E7D10",
                      "action": "allow",
                      "recommendedAction": "allow"
                    }
                  },
                  "kyc": {
                    "value": {
                      "success": true,
                      "decision": "QUARANTINED",
                      "riskScore": 60,
                      "reasons": [
                        "Test mode: kyc@test.inlineshield.io always gives QUARANTINED / require_kyc."
                      ],
                      "requestId": "AUD-MHH3K32F-7E2A4C6B8D90",
                      "action": "hold",
                      "recommendedAction": "require_kyc"
                    }
                  },
                  "noToken": {
                    "value": {
                      "success": false,
                      "error": "NO_TOKEN"
                    }
                  },
                  "invalidToken": {
                    "value": {
                      "success": false,
                      "error": "INVALID_TOKEN"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No `email`, or (without the rate-limit headers) a body that is not valid JSON.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current one-minute window. Absent when the body is not valid JSON (rejected before the limit is checked).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Absent when the body is not valid JSON (rejected before the limit is checked).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-RateLimit-Reset": {
                "description": "Seconds until the current window ends. Absent when the body is not valid JSON (rejected before the limit is checked).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "BAD_REQUEST"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "description": "The per-minute limit of this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "RATE_LIMIT_EXCEEDED"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "rateLimit": {
                    "value": {
                      "error": "RATE_LIMIT_EXCEEDED",
                      "message": "Siteverify rate limit of 1200 req/min exceeded. Try again in 12 seconds."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "VERIFICATION_ERROR",
                        "AUTH_ERROR"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/api/v1/reviews/{reviewId}": {
      "get": {
        "operationId": "getReview",
        "tags": [
          "Reviews"
        ],
        "summary": "Get a held sign-up's review",
        "description": "The status of a held sign-up: `pending` until someone decides it in the console, then `approved` or `rejected`. A review nobody decides is rejected when it expires (`decisionReason: \"expired\"`). Limited to 60 requests per minute per key; not counted against the quota. Test mode creates no reviews.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "reviewId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "rev_Hk3pQ9xZ2mV7cN4bL1kJ8hG5",
            "description": "The `reviewId` of a held screening."
          }
        ],
        "responses": {
          "200": {
            "description": "The review.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReviewStatus"
                },
                "examples": {
                  "pending": {
                    "value": {
                      "reviewId": "rev_Hk3pQ9xZ2mV7cN4bL1kJ8hG5",
                      "status": "pending",
                      "decisionReason": null,
                      "decidedAt": null,
                      "expiresAt": "2026-11-09T12:00:00.000Z"
                    }
                  },
                  "approved": {
                    "value": {
                      "reviewId": "rev_Hk3pQ9xZ2mV7cN4bL1kJ8hG5",
                      "status": "approved",
                      "decisionReason": "operator",
                      "decidedAt": "2026-11-02T14:31:07.000Z",
                      "expiresAt": "2026-11-09T12:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "404": {
            "description": "No such review in this account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "NOT_FOUND"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "NOT_FOUND",
                      "message": "Review not found."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 60 requests per minute with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "RATE_LIMIT_EXCEEDED"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "REVIEW_ERROR",
                        "AUTH_ERROR"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/api/v1/feedback": {
      "post": {
        "operationId": "sendFeedback",
        "tags": [
          "Feedback"
        ],
        "summary": "Report a screening's outcome",
        "description": "Tell InlineShield what a screened player turned out to be (`fraud` or `legit`), by the `requestId` the API returned or by the `/check` token. Accepted for screenings up to 180 days old by `requestId`; a `/check` token is kept for about a day after the screening, so later feedback must use the `requestId`. Sending the same feedback again changes nothing (`changed: false`).\n\nFeedback on a test screening is accepted but never recorded (`changed: false`). Limited per key to your plan's per-minute limit; never counted against the quota and accepted while the account is over it.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackRequest"
              },
              "examples": {
                "byRequestId": {
                  "summary": "By requestId (a test screening: accepted, not recorded)",
                  "value": {
                    "requestId": "AUD-MHH3K33H-5B9D1F3A7C02",
                    "outcome": "fraud",
                    "category": "bonus_abuse",
                    "note": "Second account, same payment card."
                  }
                },
                "byToken": {
                  "summary": "By /check token (a test screening: accepted, not recorded)",
                  "value": {
                    "token": "bst_q3Jm0b8vN2kXo7rT5wYz1cE4hL6pA9sD0fG2jK8uV3i",
                    "outcome": "legit"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted. `changed` is false when nothing new was recorded (the same feedback again, or a test screening).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackResponse"
                },
                "examples": {
                  "byRequestId": {
                    "value": {
                      "success": true,
                      "outcome": "fraud",
                      "category": "bonus_abuse",
                      "changed": false
                    }
                  },
                  "byToken": {
                    "value": {
                      "success": true,
                      "outcome": "legit",
                      "category": null,
                      "changed": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not exactly one of `requestId` or `token`, a bad `outcome`, `category` or `note`, or (without the rate-limit headers) a body that is not valid JSON.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed in the current one-minute window. Absent when the body is not valid JSON (rejected before the limit is checked).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current window. Absent when the body is not valid JSON (rejected before the limit is checked).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "X-RateLimit-Reset": {
                "description": "Seconds until the current window ends. Absent when the body is not valid JSON (rejected before the limit is checked).",
                "required": false,
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "BAD_REQUEST"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "404": {
            "description": "No such screening in this account.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "NOT_FOUND"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "422": {
            "description": "The screening is more than 180 days old.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "SCREENING_TOO_OLD"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "The per-minute limit of this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "RATE_LIMIT_EXCEEDED"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "message"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "FEEDBACK_ERROR",
                        "AUTH_ERROR"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Health"
        ],
        "summary": "Service health",
        "description": "Whether the API and its database are up. No authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "Operational.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "examples": {
                  "operational": {
                    "value": {
                      "status": "operational",
                      "database": {
                        "status": "connected"
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Degraded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "examples": {
                  "degraded": {
                    "value": {
                      "status": "degraded",
                      "database": {
                        "status": "disconnected"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "registration.allowed": {
      "post": {
        "operationId": "webhookRegistrationAllowed",
        "tags": [
          "Webhooks"
        ],
        "summary": "A sign-up was allowed",
        "description": "Sent after each live screening (/verify or /check) with the verdict ALLOWED, to endpoints subscribed to it (subscribed by default). Contains the player's email and IP. Signed with your endpoint's secret; see X-InlineShield-Signature. Live screenings only: test mode sends no webhooks.",
        "parameters": [
          {
            "name": "X-InlineShield-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. The HMAC is computed with your endpoint's signing secret (`whsec_…`, the whole string as the key) over `<t>.<raw request body>`. Compare it in constant time. `t` is the time the delivery was first signed: automatic retries resend the same signature (so `t` can be a few seconds old), and a delivery resent later (for example one interrupted by a restart on our side) is signed again with a new `t`: its content and `id` are the same, but the bytes can differ.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-InlineShield-Event",
            "in": "header",
            "required": true,
            "description": "The event name.",
            "schema": {
              "const": "registration.allowed"
            }
          },
          {
            "name": "X-InlineShield-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id (the body's `id`).",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-InlineShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Automatic deliveries: the attempt number, from 1.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-InlineShield-Retry",
            "in": "header",
            "required": false,
            "description": "A delivery resent later (for example after a restart on our side): `true` (no Attempt header then).",
            "schema": {
              "const": "true"
            }
          },
          {
            "name": "X-BetShield-Signature",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Signature, same value.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-BetShield-Event",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Event, same value.",
            "schema": {
              "const": "registration.allowed"
            }
          },
          {
            "name": "X-BetShield-Delivery",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Delivery, same value.",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-BetShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Attempt, same value.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-BetShield-Retry",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Retry, same value.",
            "schema": {
              "const": "true"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "registration.allowed"
                      },
                      "data": {
                        "$ref": "#/components/schemas/RegistrationEventData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "allowed": {
                  "value": {
                    "id": "del_8c1e4f7a2b9d3e05",
                    "event": "registration.allowed",
                    "apiVersion": "2026-10-25",
                    "createdAt": "2026-11-02T12:00:01.000Z",
                    "organizationId": "org_7d1c2b3a",
                    "data": {
                      "requestId": "AUD-MHH3K2QZ-4F1C9B2E7D10",
                      "email": "player0@example.com",
                      "ip": "203.0.113.24",
                      "decision": "ALLOWED",
                      "verdict": "ALLOWED",
                      "riskScore": 4,
                      "reasons": [],
                      "triggers": [],
                      "timestamp": "2026-11-02T12:00:00.912Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status means delivered. The response body is not read."
          },
          "default": {
            "description": "Redirects and 4xx (except 429) are not retried. 5xx, 429, timeouts and network errors are retried with backoff, up to the configured number of attempts."
          }
        }
      }
    },
    "registration.blocked": {
      "post": {
        "operationId": "webhookRegistrationBlocked",
        "tags": [
          "Webhooks"
        ],
        "summary": "A sign-up was held or blocked",
        "description": "Sent after each live screening whose verdict is not ALLOWED: BLOCKED and also QUARANTINED (read `data.decision`). Subscribed by default. Contains the player's email and IP. A held sign-up is later resolved by signup.approved or signup.rejected. Signed with your endpoint's secret; see X-InlineShield-Signature. Live screenings only: test mode sends no webhooks.",
        "parameters": [
          {
            "name": "X-InlineShield-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. The HMAC is computed with your endpoint's signing secret (`whsec_…`, the whole string as the key) over `<t>.<raw request body>`. Compare it in constant time. `t` is the time the delivery was first signed: automatic retries resend the same signature (so `t` can be a few seconds old), and a delivery resent later (for example one interrupted by a restart on our side) is signed again with a new `t`: its content and `id` are the same, but the bytes can differ.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-InlineShield-Event",
            "in": "header",
            "required": true,
            "description": "The event name.",
            "schema": {
              "const": "registration.blocked"
            }
          },
          {
            "name": "X-InlineShield-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id (the body's `id`).",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-InlineShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Automatic deliveries: the attempt number, from 1.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-InlineShield-Retry",
            "in": "header",
            "required": false,
            "description": "A delivery resent later (for example after a restart on our side): `true` (no Attempt header then).",
            "schema": {
              "const": "true"
            }
          },
          {
            "name": "X-BetShield-Signature",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Signature, same value.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-BetShield-Event",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Event, same value.",
            "schema": {
              "const": "registration.blocked"
            }
          },
          {
            "name": "X-BetShield-Delivery",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Delivery, same value.",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-BetShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Attempt, same value.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-BetShield-Retry",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Retry, same value.",
            "schema": {
              "const": "true"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "registration.blocked"
                      },
                      "data": {
                        "$ref": "#/components/schemas/RegistrationEventData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "held": {
                  "value": {
                    "id": "del_8c1e4f7a2b9d3e05",
                    "event": "registration.blocked",
                    "apiVersion": "2026-10-25",
                    "createdAt": "2026-11-02T12:00:01.000Z",
                    "organizationId": "org_7d1c2b3a",
                    "data": {
                      "requestId": "AUD-MHH3K30A-91B7C2D40E55",
                      "email": "player1@example.com",
                      "ip": "203.0.113.24",
                      "decision": "QUARANTINED",
                      "verdict": "QUARANTINED",
                      "riskScore": 62,
                      "reasons": [
                        "Plus-addressed email (player+bonus@…)."
                      ],
                      "triggers": [
                        "PLUS_ALIASED_EMAIL"
                      ],
                      "timestamp": "2026-11-02T12:00:00.912Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status means delivered. The response body is not read."
          },
          "default": {
            "description": "Redirects and 4xx (except 429) are not retried. 5xx, 429, timeouts and network errors are retried with backoff, up to the configured number of attempts."
          }
        }
      }
    },
    "signup.approved": {
      "post": {
        "operationId": "webhookSignupApproved",
        "tags": [
          "Webhooks"
        ],
        "summary": "A held sign-up was approved",
        "description": "Sent when a held sign-up is approved in the console. Sent to every enabled endpoint, whatever its subscriptions. No email or IP. Signed with your endpoint's secret; see X-InlineShield-Signature. Live screenings only: test mode sends no webhooks.",
        "parameters": [
          {
            "name": "X-InlineShield-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. The HMAC is computed with your endpoint's signing secret (`whsec_…`, the whole string as the key) over `<t>.<raw request body>`. Compare it in constant time. `t` is the time the delivery was first signed: automatic retries resend the same signature (so `t` can be a few seconds old), and a delivery resent later (for example one interrupted by a restart on our side) is signed again with a new `t`: its content and `id` are the same, but the bytes can differ.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-InlineShield-Event",
            "in": "header",
            "required": true,
            "description": "The event name.",
            "schema": {
              "const": "signup.approved"
            }
          },
          {
            "name": "X-InlineShield-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id (the body's `id`).",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-InlineShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Automatic deliveries: the attempt number, from 1.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-InlineShield-Retry",
            "in": "header",
            "required": false,
            "description": "A delivery resent later (for example after a restart on our side): `true` (no Attempt header then).",
            "schema": {
              "const": "true"
            }
          },
          {
            "name": "X-BetShield-Signature",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Signature, same value.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-BetShield-Event",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Event, same value.",
            "schema": {
              "const": "signup.approved"
            }
          },
          {
            "name": "X-BetShield-Delivery",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Delivery, same value.",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-BetShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Attempt, same value.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-BetShield-Retry",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Retry, same value.",
            "schema": {
              "const": "true"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "signup.approved"
                      },
                      "data": {
                        "$ref": "#/components/schemas/SignupEventData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "approved": {
                  "value": {
                    "id": "del_5f2c9a1e7b3d4c60",
                    "event": "signup.approved",
                    "apiVersion": "2026-10-25",
                    "createdAt": "2026-11-02T14:31:08.000Z",
                    "organizationId": "org_7d1c2b3a",
                    "data": {
                      "event": "signup.approved",
                      "reviewId": "rev_Hk3pQ9xZ2mV7cN4bL1kJ8hG5",
                      "requestId": "AUD-MHH3K31C-0C3D5E7F9A21",
                      "status": "approved",
                      "decisionReason": "operator",
                      "decidedAt": "2026-11-02T14:31:07.000Z",
                      "recommendedAction": "allow"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status means delivered. The response body is not read."
          },
          "default": {
            "description": "Redirects and 4xx (except 429) are not retried. 5xx, 429, timeouts and network errors are retried with backoff, up to the configured number of attempts."
          }
        }
      }
    },
    "signup.rejected": {
      "post": {
        "operationId": "webhookSignupRejected",
        "tags": [
          "Webhooks"
        ],
        "summary": "A held sign-up was rejected",
        "description": "Sent when a held sign-up is rejected in the console, or expires undecided (`decisionReason: \"expired\"`). Sent to every enabled endpoint. No email or IP. Signed with your endpoint's secret; see X-InlineShield-Signature. Live screenings only: test mode sends no webhooks.",
        "parameters": [
          {
            "name": "X-InlineShield-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. The HMAC is computed with your endpoint's signing secret (`whsec_…`, the whole string as the key) over `<t>.<raw request body>`. Compare it in constant time. `t` is the time the delivery was first signed: automatic retries resend the same signature (so `t` can be a few seconds old), and a delivery resent later (for example one interrupted by a restart on our side) is signed again with a new `t`: its content and `id` are the same, but the bytes can differ.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-InlineShield-Event",
            "in": "header",
            "required": true,
            "description": "The event name.",
            "schema": {
              "const": "signup.rejected"
            }
          },
          {
            "name": "X-InlineShield-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id (the body's `id`).",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-InlineShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Automatic deliveries: the attempt number, from 1.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-InlineShield-Retry",
            "in": "header",
            "required": false,
            "description": "A delivery resent later (for example after a restart on our side): `true` (no Attempt header then).",
            "schema": {
              "const": "true"
            }
          },
          {
            "name": "X-BetShield-Signature",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Signature, same value.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-BetShield-Event",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Event, same value.",
            "schema": {
              "const": "signup.rejected"
            }
          },
          {
            "name": "X-BetShield-Delivery",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Delivery, same value.",
            "schema": {
              "type": "string",
              "pattern": "^del_"
            }
          },
          {
            "name": "X-BetShield-Attempt",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Attempt, same value.",
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]*$"
            }
          },
          {
            "name": "X-BetShield-Retry",
            "in": "header",
            "required": false,
            "description": "Old name of X-InlineShield-Retry, same value.",
            "schema": {
              "const": "true"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "signup.rejected"
                      },
                      "data": {
                        "$ref": "#/components/schemas/SignupEventData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "rejected": {
                  "value": {
                    "id": "del_5f2c9a1e7b3d4c60",
                    "event": "signup.rejected",
                    "apiVersion": "2026-10-25",
                    "createdAt": "2026-11-02T14:31:08.000Z",
                    "organizationId": "org_7d1c2b3a",
                    "data": {
                      "event": "signup.rejected",
                      "reviewId": "rev_Hk3pQ9xZ2mV7cN4bL1kJ8hG5",
                      "requestId": "AUD-MHH3K31C-0C3D5E7F9A21",
                      "status": "rejected",
                      "decisionReason": "operator",
                      "decidedAt": "2026-11-02T14:31:07.000Z",
                      "recommendedAction": "block"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx status means delivered. The response body is not read."
          },
          "default": {
            "description": "Redirects and 4xx (except 429) are not retried. 5xx, 429, timeouts and network errors are retried with backoff, up to the configured number of attempts."
          }
        }
      }
    },
    "test.ping": {
      "post": {
        "operationId": "webhookTestPing",
        "tags": [
          "Webhooks"
        ],
        "summary": "Test ping",
        "description": "Sent by \"Send test ping\" in the console (Settings › Webhook), signed like every delivery. Its body has a different shape from the other events: no `id`, `apiVersion` or `createdAt`, and a numeric `timestamp` (the same value as `t` in the signature). Sent once, never retried.",
        "parameters": [
          {
            "name": "X-InlineShield-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256>`. The HMAC is computed with your endpoint's signing secret (`whsec_…`, the whole string as the key) over `<t>.<raw request body>`. Compare it in constant time. `t` is the time the delivery was first signed: automatic retries resend the same signature (so `t` can be a few seconds old), and a delivery resent later (for example one interrupted by a restart on our side) is signed again with a new `t`: its content and `id` are the same, but the bytes can differ.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-InlineShield-Event",
            "in": "header",
            "required": true,
            "description": "The event name.",
            "schema": {
              "const": "test.ping"
            }
          },
          {
            "name": "X-BetShield-Signature",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Signature, same value.",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          },
          {
            "name": "X-BetShield-Event",
            "in": "header",
            "required": true,
            "description": "Old name of X-InlineShield-Event, same value.",
            "schema": {
              "const": "test.ping"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "event",
                  "timestamp",
                  "organizationId",
                  "data"
                ],
                "additionalProperties": false,
                "properties": {
                  "event": {
                    "const": "test.ping"
                  },
                  "timestamp": {
                    "type": "integer"
                  },
                  "organizationId": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "required": [
                      "message"
                    ],
                    "additionalProperties": false,
                    "properties": {
                      "message": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "examples": {
                "ping": {
                  "value": {
                    "event": "test.ping",
                    "timestamp": 1793620805,
                    "organizationId": "org_7d1c2b3a",
                    "data": {
                      "message": "InlineShield verified test delivery probe"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "The ping is reported as delivered in the console."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Your secret API key (`bs_live_…` or `bs_test_…`), from the console under Settings › Keys. Never put it in a browser or an app. The header name `X-ApiKey` is also accepted (deprecated)."
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Requests allowed in the current one-minute window.",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "description": "Seconds until the current window ends.",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-Quota-Limit": {
        "description": "Live requests only: the monthly quota of the plan.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-Quota-Used": {
        "description": "Live requests only: screenings counted this month, this one included.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-Quota-Reset": {
        "description": "Live requests only: when the count resets (the start of the next UTC month).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "X-Quota-State": {
        "description": "Live requests only: `ok`, `warn` (80% used), `limit` (100%), `grace` (over 100%, still served) or `blocked`.",
        "schema": {
          "type": "string",
          "enum": [
            "ok",
            "warn",
            "limit",
            "grace",
            "blocked"
          ]
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before trying again.",
        "required": true,
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No `X-Api-Key` header, or an unknown, revoked or expired key.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "message"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "UNAUTHORIZED",
                    "INVALID_API_KEY",
                    "EXPIRED_API_KEY",
                    "ORGANIZATION_NOT_FOUND"
                  ]
                },
                "message": {
                  "type": "string"
                }
              },
              "additionalProperties": false
            },
            "examples": {
              "missing": {
                "value": {
                  "error": "UNAUTHORIZED",
                  "message": "Access denied: Missing required X-Api-Key header."
                }
              },
              "invalid": {
                "value": {
                  "error": "INVALID_API_KEY",
                  "message": "Invalid API key provided."
                }
              }
            }
          }
        }
      },
      "Suspended": {
        "description": "The account is suspended.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/SuspendedError"
            }
          }
        }
      },
      "AuthUnavailable": {
        "description": "The key could not be checked right now. Try again.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "message"
              ],
              "properties": {
                "error": {
                  "type": "string",
                  "enum": [
                    "DATABASE_ERROR"
                  ]
                },
                "message": {
                  "type": "string"
                }
              },
              "additionalProperties": false
            }
          }
        }
      }
    },
    "schemas": {
      "Verdict": {
        "type": "string",
        "enum": [
          "ALLOWED",
          "QUARANTINED",
          "BLOCKED"
        ],
        "description": "ALLOWED: let the player in. QUARANTINED: hold for review. BLOCKED: refuse."
      },
      "Action": {
        "type": "string",
        "enum": [
          "allow",
          "hold",
          "block"
        ],
        "description": "What to do with the sign-up: ALLOWED → allow, QUARANTINED → hold, BLOCKED → block."
      },
      "RecommendedAction": {
        "type": "string",
        "enum": [
          "allow",
          "allow_no_bonus",
          "review",
          "block",
          "require_kyc"
        ],
        "description": "Advice next to `action`: allow, allow without a sign-up bonus, review, block, or ask for KYC before paying out."
      },
      "RiskScore": {
        "type": "number",
        "minimum": 0,
        "maximum": 100,
        "description": "0 (lowest risk) to 100."
      },
      "RequestId": {
        "type": "string",
        "pattern": "^AUD-",
        "description": "The screening's id. Use it for feedback and support."
      },
      "ReviewId": {
        "type": "string",
        "pattern": "^rev_"
      },
      "Reasons": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Plain-language reasons for the result."
      },
      "Triggers": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Machine-readable signal codes (e.g. DISPOSABLE_DOMAIN, TEST_MAGIC_ADDRESS). New codes may be added."
      },
      "QuotaExceeded": {
        "type": "object",
        "required": [
          "error",
          "message",
          "used",
          "limit",
          "resetsAt"
        ],
        "additionalProperties": false,
        "properties": {
          "error": {
            "const": "QUOTA_EXCEEDED"
          },
          "message": {
            "type": "string"
          },
          "used": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 0
          },
          "resetsAt": {
            "type": "string",
            "format": "date-time",
            "description": "The start of the next UTC month."
          }
        }
      },
      "SuspendedError": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "TENANT_SUSPENDED"
            ]
          },
          "message": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "VerifyRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "minLength": 1,
            "description": "The address the player signed up with."
          },
          "ip": {
            "type": "string",
            "description": "The player's IP address (IPv4 or IPv6). Without it, the IP of the calling connection is used, which is your server's. Anything that is not an IP address gets 400 INVALID_IP."
          },
          "userAgent": {
            "type": "string",
            "description": "The player's browser User-Agent. Only this field is read; the request's own User-Agent header (your HTTP client) is ignored. Without it the User-Agent counts as empty."
          },
          "username": {
            "type": "string",
            "description": "The username the player chose, if any."
          },
          "clientDwellMs": {
            "type": "number",
            "description": "Milliseconds the sign-up form was open before it was submitted, if you measure it. Not measured means unknown, never a fast submission."
          },
          "honeypot": {
            "type": "string",
            "description": "The value of a hidden form field that people leave empty. A non-empty value is a bot signal."
          }
        },
        "additionalProperties": true,
        "description": "Other fields are accepted and ignored."
      },
      "VerifyResponse": {
        "type": "object",
        "required": [
          "requestId",
          "decision",
          "action",
          "recommendedAction",
          "verdict",
          "riskScore",
          "reasons",
          "triggers",
          "policyVersion",
          "processingTimeMs",
          "forwardedToDatabase",
          "emailAnalysis",
          "botAnalysis",
          "ipRisk"
        ],
        "additionalProperties": false,
        "properties": {
          "requestId": {
            "$ref": "#/components/schemas/RequestId"
          },
          "decision": {
            "$ref": "#/components/schemas/Verdict"
          },
          "action": {
            "$ref": "#/components/schemas/Action"
          },
          "recommendedAction": {
            "$ref": "#/components/schemas/RecommendedAction"
          },
          "reviewId": {
            "$ref": "#/components/schemas/ReviewId",
            "description": "Live holds only (`action: \"hold\"`): the review to poll or wait for."
          },
          "verdict": {
            "$ref": "#/components/schemas/Verdict",
            "description": "Same as `decision` (kept for older integrations)."
          },
          "riskScore": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "reasons": {
            "$ref": "#/components/schemas/Reasons"
          },
          "triggers": {
            "$ref": "#/components/schemas/Triggers"
          },
          "policyVersion": {
            "type": "string",
            "const": "1"
          },
          "processingTimeMs": {
            "type": "number",
            "minimum": 0
          },
          "forwardedToDatabase": {
            "type": "boolean",
            "description": "True when the decision is ALLOWED."
          },
          "emailAnalysis": {
            "$ref": "#/components/schemas/EmailAnalysis"
          },
          "botAnalysis": {
            "$ref": "#/components/schemas/BotAnalysis"
          },
          "ipRisk": {
            "$ref": "#/components/schemas/IpRisk"
          }
        }
      },
      "DnsStatus": {
        "type": "string",
        "enum": [
          "found",
          "absent",
          "unknown"
        ],
        "description": "`absent` only on a definite DNS answer; `unknown` on any lookup failure."
      },
      "EmailAnalysis": {
        "type": "object",
        "required": [
          "normalizedEmail",
          "localPart",
          "domain",
          "isDisposable",
          "isPlusAliased",
          "isRoleAccount",
          "hasTypoRisk",
          "mxValid",
          "mx",
          "mxRecords",
          "mailServers",
          "spf",
          "dmarc",
          "dkim"
        ],
        "additionalProperties": false,
        "properties": {
          "normalizedEmail": {
            "type": "string"
          },
          "localPart": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "isDisposable": {
            "type": "boolean"
          },
          "disposableCategory": {
            "type": "string"
          },
          "disposableProvider": {
            "type": "string"
          },
          "isPlusAliased": {
            "type": "boolean"
          },
          "isRoleAccount": {
            "type": "boolean"
          },
          "hasTypoRisk": {
            "type": "boolean"
          },
          "suggestedCorrection": {
            "type": "string"
          },
          "mxValid": {
            "type": "boolean",
            "description": "True only when `mx.status` is `found`."
          },
          "mx": {
            "type": "object",
            "required": [
              "status",
              "records"
            ],
            "additionalProperties": false,
            "properties": {
              "status": {
                "$ref": "#/components/schemas/DnsStatus"
              },
              "records": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MxRecord"
                }
              }
            }
          },
          "mxRecords": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MxRecord"
            },
            "description": "Same as `mx.records`."
          },
          "mailServers": {
            "type": "string",
            "enum": [
              "present",
              "none",
              "unknown"
            ]
          },
          "spf": {
            "type": "object",
            "required": [
              "exists",
              "status",
              "qualifier",
              "isPermissive",
              "mechanisms"
            ],
            "additionalProperties": false,
            "properties": {
              "exists": {
                "type": "boolean"
              },
              "status": {
                "$ref": "#/components/schemas/DnsStatus"
              },
              "raw": {
                "type": "string"
              },
              "qualifier": {
                "type": "string",
                "enum": [
                  "PASS",
                  "NEUTRAL",
                  "FAIL",
                  "NONE"
                ]
              },
              "isPermissive": {
                "type": "boolean"
              },
              "mechanisms": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "dmarc": {
            "type": "object",
            "required": [
              "exists",
              "status",
              "policy",
              "compliant"
            ],
            "additionalProperties": false,
            "properties": {
              "exists": {
                "type": "boolean"
              },
              "status": {
                "$ref": "#/components/schemas/DnsStatus"
              },
              "raw": {
                "type": "string"
              },
              "policy": {
                "type": "string",
                "enum": [
                  "reject",
                  "quarantine",
                  "none",
                  "missing"
                ]
              },
              "compliant": {
                "type": "boolean"
              },
              "percentage": {
                "type": "number"
              }
            }
          },
          "dkim": {
            "type": "object",
            "required": [
              "selectorChecked",
              "exists",
              "status"
            ],
            "additionalProperties": false,
            "properties": {
              "selectorChecked": {
                "type": "string"
              },
              "exists": {
                "type": "boolean"
              },
              "raw": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "VERIFIED",
                  "FOUND",
                  "NOT_FOUND",
                  "UNVERIFIABLE"
                ]
              }
            }
          },
          "providerType": {
            "type": "string",
            "enum": [
              "FREE_WEBMAIL",
              "DISPOSABLE",
              "CORPORATE_PRIVATE"
            ]
          },
          "isCatchAll": {
            "type": "boolean"
          },
          "localPartRandomness": {
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "random",
                  "score"
                ],
                "additionalProperties": false,
                "properties": {
                  "random": {
                    "type": "boolean"
                  },
                  "score": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "MxRecord": {
        "type": "object",
        "required": [
          "exchange",
          "priority"
        ],
        "additionalProperties": false,
        "properties": {
          "exchange": {
            "type": "string"
          },
          "priority": {
            "type": "number"
          }
        }
      },
      "BotAnalysis": {
        "type": "object",
        "required": [
          "dwellTimeMs",
          "isFastSubmission",
          "honeypotTriggered",
          "isHeadlessBrowser",
          "isAutomationAgent",
          "scriptedUserAgent",
          "scriptedClient",
          "entropyScore"
        ],
        "additionalProperties": false,
        "properties": {
          "dwellTimeMs": {
            "type": [
              "number",
              "null"
            ],
            "description": "null when not measured (never a fast submission)."
          },
          "isFastSubmission": {
            "type": "boolean"
          },
          "honeypotTriggered": {
            "type": "boolean"
          },
          "trapFieldTriggered": {
            "type": "boolean"
          },
          "trapFieldValue": {
            "type": "string"
          },
          "isHeadlessBrowser": {
            "type": "boolean"
          },
          "headlessUserAgent": {
            "type": "boolean"
          },
          "automationSignals": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isAutomationAgent": {
            "type": "boolean"
          },
          "scriptedUserAgent": {
            "type": "boolean",
            "description": "The User-Agent is a known scripted HTTP client (curl, python-requests, …)."
          },
          "scriptedClient": {
            "type": [
              "string",
              "null"
            ]
          },
          "entropyScore": {
            "type": "number"
          }
        }
      },
      "IpRisk": {
        "type": "object",
        "required": [
          "ip",
          "isDatacenter",
          "isTorExit",
          "isVpnOrProxy",
          "isVpn",
          "country",
          "countryCode",
          "org",
          "asn"
        ],
        "additionalProperties": false,
        "properties": {
          "ip": {
            "type": "string"
          },
          "isDatacenter": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "null = unknown."
          },
          "isTorExit": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "null = unknown."
          },
          "isVpnOrProxy": {
            "type": "boolean",
            "description": "isDatacenter or isTorExit is true."
          },
          "isVpn": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "On the VPN provider list; null = unknown."
          },
          "country": {
            "type": "string",
            "description": "Country name, or \"Unknown\"."
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "countryName": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "flagEmoji": {
            "type": "string"
          },
          "org": {
            "type": "string",
            "description": "The network owner, or \"Unknown\"."
          },
          "asOrg": {
            "type": [
              "string",
              "null"
            ]
          },
          "asn": {
            "type": [
              "string",
              "null"
            ],
            "description": "\"AS<number>\", or null."
          },
          "isp": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CheckRequest": {
        "type": "object",
        "required": [
          "siteKey",
          "email"
        ],
        "properties": {
          "siteKey": {
            "type": "string",
            "description": "Your site key (`bs_site_…`, or `bs_site_test_…` for test mode)."
          },
          "email": {
            "type": "string",
            "minLength": 1
          },
          "username": {
            "type": "string"
          },
          "clientDwellMs": {
            "type": "number",
            "description": "Milliseconds the form was open before it was submitted."
          },
          "honeypot": {
            "type": "string",
            "description": "The value of a hidden field people leave empty."
          },
          "signals": {
            "type": "object",
            "description": "Browser signals collected by inlineshield.js. Invalid values are ignored.",
            "properties": {
              "webdriver": {
                "type": "boolean"
              },
              "tz": {
                "type": "string",
                "maxLength": 64,
                "description": "IANA time zone, e.g. Europe/Berlin."
              },
              "langs": {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 35
                },
                "description": "Up to 5 browser languages are kept."
              },
              "automationHints": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "webdriver",
                    "cdc_props",
                    "no_languages",
                    "headless_ua_ch",
                    "phantom",
                    "nightmare",
                    "selenium_props"
                  ]
                }
              }
            }
          }
        },
        "additionalProperties": true,
        "description": "Other fields are ignored. The IP and User-Agent always come from the request."
      },
      "CheckResponse": {
        "type": "object",
        "required": [
          "decision",
          "riskScore",
          "reasons",
          "requestId",
          "token"
        ],
        "additionalProperties": false,
        "properties": {
          "decision": {
            "$ref": "#/components/schemas/Verdict"
          },
          "riskScore": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "reasons": {
            "$ref": "#/components/schemas/Reasons"
          },
          "requestId": {
            "$ref": "#/components/schemas/RequestId"
          },
          "token": {
            "type": "string",
            "pattern": "^bst_",
            "description": "Single use, valid for 10 minutes: send it to /siteverify from your backend."
          }
        }
      },
      "SiteverifyRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "The token from /check (inlineshield.js puts it in the form)."
          },
          "email": {
            "type": "string",
            "minLength": 1,
            "description": "The email the player submitted. Must be the one that was checked."
          }
        },
        "additionalProperties": true
      },
      "SiteverifyResponse": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/SiteverifySuccess"
          },
          {
            "$ref": "#/components/schemas/SiteverifyFailure"
          }
        ]
      },
      "SiteverifySuccess": {
        "type": "object",
        "required": [
          "success",
          "decision",
          "riskScore",
          "reasons",
          "requestId",
          "action",
          "recommendedAction"
        ],
        "additionalProperties": false,
        "properties": {
          "success": {
            "const": true
          },
          "decision": {
            "$ref": "#/components/schemas/Verdict"
          },
          "riskScore": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "reasons": {
            "$ref": "#/components/schemas/Reasons"
          },
          "requestId": {
            "$ref": "#/components/schemas/RequestId"
          },
          "action": {
            "$ref": "#/components/schemas/Action"
          },
          "recommendedAction": {
            "$ref": "#/components/schemas/RecommendedAction"
          },
          "reviewId": {
            "$ref": "#/components/schemas/ReviewId",
            "description": "Live holds only."
          }
        }
      },
      "SiteverifyFailure": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "additionalProperties": false,
        "properties": {
          "success": {
            "const": false
          },
          "error": {
            "type": "string",
            "enum": [
              "NO_TOKEN",
              "INVALID_TOKEN",
              "EXPIRED_TOKEN",
              "TOKEN_ALREADY_USED",
              "EMAIL_MISMATCH",
              "QUOTA_EXCEEDED"
            ],
            "description": "NO_TOKEN: no token was sent. INVALID_TOKEN: not a token of this account. EXPIRED_TOKEN: older than 10 minutes. TOKEN_ALREADY_USED: redeemed before. EMAIL_MISMATCH: issued for another email. QUOTA_EXCEEDED: no token was sent and the account is over its quota."
          }
        }
      },
      "FeedbackRequest": {
        "type": "object",
        "required": [
          "outcome"
        ],
        "oneOf": [
          {
            "type": "object",
            "required": [
              "requestId"
            ]
          },
          {
            "type": "object",
            "required": [
              "token"
            ]
          }
        ],
        "properties": {
          "requestId": {
            "type": "string",
            "description": "The requestId the API returned."
          },
          "token": {
            "type": "string",
            "description": "The /check token. A token is kept for about a day after the screening; for later feedback send the requestId (/siteverify returns it)."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "fraud",
              "legit"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "bonus_abuse",
              "multi_account",
              "chargeback",
              "kyc_failed",
              "bot",
              "good_customer",
              "other",
              null
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "At most 200 characters after trimming. Never shown to other operators."
          }
        },
        "additionalProperties": true,
        "description": "Send exactly one of `requestId` or `token`."
      },
      "FeedbackResponse": {
        "type": "object",
        "required": [
          "success",
          "outcome",
          "category",
          "changed"
        ],
        "additionalProperties": false,
        "properties": {
          "success": {
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "fraud",
              "legit"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "bonus_abuse",
              "multi_account",
              "chargeback",
              "kyc_failed",
              "bot",
              "good_customer",
              "other",
              null
            ]
          },
          "changed": {
            "type": "boolean"
          }
        }
      },
      "ReviewStatus": {
        "type": "object",
        "required": [
          "reviewId",
          "status",
          "decisionReason",
          "decidedAt",
          "expiresAt"
        ],
        "additionalProperties": false,
        "properties": {
          "reviewId": {
            "$ref": "#/components/schemas/ReviewId"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected"
            ]
          },
          "decisionReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "operator",
              "expired",
              null
            ],
            "description": "operator: decided in the console. expired: nobody decided in time (rejected)."
          },
          "decidedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "database"
        ],
        "additionalProperties": false,
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "operational",
              "degraded"
            ]
          },
          "database": {
            "type": "object",
            "required": [
              "status"
            ],
            "additionalProperties": false,
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "connected",
                  "disconnected"
                ]
              }
            }
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "required": [
          "id",
          "event",
          "apiVersion",
          "createdAt",
          "organizationId",
          "data"
        ],
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^del_",
            "description": "The delivery id (also in X-InlineShield-Delivery). Retries of a delivery keep it."
          },
          "event": {
            "type": "string"
          },
          "apiVersion": {
            "const": "2026-10-25"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "organizationId": {
            "type": "string"
          },
          "data": {
            "type": "object"
          }
        }
      },
      "RegistrationEventData": {
        "type": "object",
        "required": [
          "requestId",
          "email",
          "ip",
          "decision",
          "verdict",
          "riskScore",
          "reasons",
          "triggers",
          "timestamp"
        ],
        "additionalProperties": false,
        "properties": {
          "requestId": {
            "$ref": "#/components/schemas/RequestId"
          },
          "email": {
            "type": "string"
          },
          "ip": {
            "type": "string"
          },
          "decision": {
            "$ref": "#/components/schemas/Verdict"
          },
          "verdict": {
            "$ref": "#/components/schemas/Verdict"
          },
          "riskScore": {
            "$ref": "#/components/schemas/RiskScore"
          },
          "reasons": {
            "$ref": "#/components/schemas/Reasons"
          },
          "triggers": {
            "$ref": "#/components/schemas/Triggers"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SignupEventData": {
        "type": "object",
        "required": [
          "event",
          "reviewId",
          "requestId",
          "status",
          "decisionReason",
          "decidedAt",
          "recommendedAction"
        ],
        "additionalProperties": false,
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "signup.approved",
              "signup.rejected"
            ]
          },
          "reviewId": {
            "$ref": "#/components/schemas/ReviewId"
          },
          "requestId": {
            "type": "string",
            "description": "The requestId the screening response carried."
          },
          "status": {
            "type": "string",
            "enum": [
              "approved",
              "rejected"
            ]
          },
          "decisionReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "operator",
              "expired",
              null
            ]
          },
          "decidedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "recommendedAction": {
            "type": "string",
            "enum": [
              "allow",
              "require_kyc",
              "block"
            ],
            "description": "signup.rejected: block. signup.approved: require_kyc when the hold's signals call for it, else allow."
          }
        }
      }
    }
  },
  "x-webhooks-not-sent": {
    "description": "Event names accepted in an endpoint's subscription list but never sent. A hold arrives as registration.blocked (data.decision QUARANTINED), then signup.approved or signup.rejected.",
    "events": [
      "incident.created",
      "registration.review_required"
    ]
  },
  "x-webhook-signature-example": {
    "description": "A worked example: HMAC-SHA256 over \"<timestamp>.<body>\" with the secret gives the header value.",
    "secret": "whsec_ZXhhbXBsZS1zZWNyZXQtZG8tbm90LXVzZS1pbi1wcm9kdWN0aW9u",
    "timestamp": 1793620805,
    "body": "{\"id\":\"del_5f2c9a1e7b3d4c60\",\"event\":\"signup.approved\",\"apiVersion\":\"2026-10-25\",\"createdAt\":\"2026-11-02T12:00:05.000Z\",\"organizationId\":\"org_7d1c2b3a\",\"data\":{\"event\":\"signup.approved\",\"reviewId\":\"rev_Hk3pQ9xZ2mV7cN4bL1kJ8hG5\",\"requestId\":\"AUD-MHH3K2QZ-4F1C9B2E7D10\",\"status\":\"approved\",\"decisionReason\":\"operator\",\"decidedAt\":\"2026-11-02T12:00:04.000Z\",\"recommendedAction\":\"allow\"}}",
    "header": "t=1793620805,v1=0b14a41708b9a46125d31465e106465d80990634e175f4d310bc939e1ba7fa7a"
  }
}
