{
  "openapi": "3.1.0",
  "info": {
    "title": "TrustVerify Public API",
    "version": "1.1.0",
    "description": "REST API for identity (KYC) checks, UK company (KYB) checks, AML/PEP/sanctions screening, reports and webhooks. Authenticate with an API key in the Authorization header (Bearer). Test keys (tv_test_sk_…) only ever return sandbox demo data and never run real or paid checks. Docs: https://trustverify.co.uk/docs",
    "contact": {
      "name": "TrustVerify developer support",
      "url": "https://trustverify.co.uk/contact"
    },
    "termsOfService": "https://trustverify.co.uk/terms"
  },
  "servers": [
    {
      "url": "https://api.trustverify.co.uk",
      "description": "Production and sandbox (the key decides which)"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Validate keys and exchange them for short-lived tokens."
    },
    {
      "name": "KYC",
      "description": "Identity checks: ID document, selfie and liveness."
    },
    {
      "name": "KYB",
      "description": "UK company checks against Companies House."
    },
    {
      "name": "AML",
      "description": "Sanctions and PEP screening, and ongoing monitoring."
    },
    {
      "name": "Checks",
      "description": "Read check results."
    },
    {
      "name": "Workflows",
      "description": "Run a saved workflow."
    },
    {
      "name": "Reports",
      "description": "Compliance reports and evidence bundles."
    },
    {
      "name": "Webhooks",
      "description": "Register endpoints that receive signed events."
    }
  ],
  "paths": {
    "/api/v1/kyc/verify": {
      "post": {
        "tags": [
          "KYC"
        ],
        "summary": "Run a full KYC verification via API key",
        "description": "Accepts base64-encoded document and selfie images. Runs face matching, optional document corroboration and AML screening, and returns a synchronous result. Liveness is covered by the guided selfie step; the liveness signal reports what was returned for this check.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "documentImage": {
                    "type": "string",
                    "description": "Base64 document front (single-image mode)",
                    "example": "/9j/4AAQ…"
                  },
                  "documentImageFront": {
                    "type": "string",
                    "description": "Base64 document front (named mode — either this or documentImage is required)",
                    "example": "/9j/4AAQ…"
                  },
                  "documentImageBack": {
                    "type": "string",
                    "description": "Base64 document back (optional)"
                  },
                  "selfieImage": {
                    "type": "string",
                    "description": "Base64-encoded JPEG/PNG selfie",
                    "example": "/9j/4AAQ…"
                  },
                  "documentType": {
                    "type": "string",
                    "enum": [
                      "passport",
                      "national_id",
                      "driving_licence"
                    ],
                    "default": "passport",
                    "example": "passport"
                  },
                  "checks": {
                    "type": "object",
                    "properties": {
                      "amlScreening": {
                        "type": "boolean"
                      }
                    }
                  },
                  "generateReport": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include a reportUrl (RPT-001 PDF) in the response"
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {},
                    "description": "Echoed back on the result. metadata.webhookUrl is not called: per-request callbacks are paused, and the response carries callback \"not_sent\".",
                    "example": {
                      "firstName": "Jane",
                      "lastName": "Smith",
                      "dateOfBirth": "1990-05-20",
                      "country": "GB"
                    }
                  }
                },
                "required": [
                  "selfieImage"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "KYC verification result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "checkId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "passed",
                        "failed",
                        "review"
                      ]
                    },
                    "score": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Deterministic confidence, bounded by the weakest piece of evidence actually returned — reproducible from the same provider outputs. null when the providers returned verdicts but no numeric scores: the decision stands on the boolean signals, and no confidence was measured. Do not read null as zero.",
                      "example": 0.941
                    },
                    "riskLevel": {
                      "type": "string",
                      "enum": [
                        "low",
                        "medium",
                        "high"
                      ]
                    },
                    "documentType": {
                      "type": "string"
                    },
                    "completedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "reportUrl": {
                      "type": "string",
                      "description": "Present when generateReport was true"
                    },
                    "signals": {
                      "type": "object",
                      "properties": {
                        "biometrics": {
                          "type": "object",
                          "properties": {
                            "faceMatch": {
                              "type": [
                                "boolean",
                                "null"
                              ]
                            },
                            "liveness": {
                              "type": [
                                "boolean",
                                "null"
                              ]
                            }
                          },
                          "required": [
                            "faceMatch",
                            "liveness"
                          ]
                        },
                        "aml": {
                          "type": "object",
                          "properties": {
                            "clear": {
                              "type": [
                                "boolean",
                                "null"
                              ]
                            },
                            "riskLevel": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          },
                          "required": [
                            "clear",
                            "riskLevel"
                          ]
                        }
                      },
                      "required": [
                        "biometrics",
                        "aml"
                      ],
                      "description": "A signal is null when no provider returned a result for it. null is never treated as a pass: `passed` requires an explicit true from the face match, plus an AML clear when screening was requested. The liveness signal is null when no liveness result was returned for the check, and null never changes the status. It can still FAIL a check: an explicit liveness false returns `failed`."
                    },
                    "providers": {
                      "$ref": "#/components/schemas/ProviderStatusMap"
                    },
                    "metadata": {
                      "type": "object",
                      "additionalProperties": {}
                    },
                    "callback": {
                      "type": "string",
                      "enum": [
                        "not_sent"
                      ],
                      "description": "Present when the request carried a webhookUrl: per-request callbacks are paused and nothing was sent. The result is in this response."
                    },
                    "callbackReason": {
                      "type": "string",
                      "example": "Callbacks to a per-request webhookUrl are paused and nothing was sent. The result is in this response."
                    }
                  },
                  "required": [
                    "checkId",
                    "status",
                    "score",
                    "riskLevel",
                    "documentType",
                    "completedAt",
                    "signals",
                    "providers",
                    "metadata"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Validation error — including checks.amlScreening enabled without a name in metadata"
          },
          "401": {
            "description": "Invalid API key"
          },
          "429": {
            "description": "API rate limit exceeded"
          },
          "503": {
            "description": "A required provider did not return a result, so no verdict is issued. The response carries `unavailableProviders`, the full `providers` map and the partial `signals`. This is never a failed verification — it means the check did not run. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "verification_unavailable"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "unavailableProviders": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": []
                    },
                    "providers": {
                      "$ref": "#/components/schemas/ProviderStatusMap"
                    },
                    "signals": {
                      "type": "object",
                      "additionalProperties": {}
                    },
                    "requestId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error",
                    "message",
                    "unavailableProviders",
                    "providers",
                    "signals",
                    "requestId"
                  ]
                }
              }
            }
          }
        },
        "operationId": "postKycVerify"
      }
    },
    "/api/v1/kyb/verify": {
      "post": {
        "tags": [
          "KYB"
        ],
        "summary": "Verify a business against the Companies House register (API key)",
        "description": "Either companyNumber or companyName is required. The company is verified against the live Companies House register: a companyNumber resolves directly; a companyName must clear a similarity threshold against the registered name — below it, ranked \"did you mean\" candidates are returned instead of a guess. Registry-verified companies (registered name + active officers) are then screened against sanctions and PEP lists with material-hit scoring, and an LEI register lookup runs when lei is supplied or checks.leiLookup is true. Statuses: passed (registry-verified, active, screens clear) · review (verified but flagged, or screening unavailable) · failed (not on the register) · unverified (registry unavailable, ambiguous name, or unsupported jurisdiction — never a pass). Test-mode keys receive deterministic sandbox fixtures labelled environment:\"sandbox\".",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "companyNumber": {
                    "type": "string",
                    "example": "00445790"
                  },
                  "companyName": {
                    "type": "string",
                    "example": "TESCO PLC"
                  },
                  "lei": {
                    "type": "string",
                    "description": "LEI to verify in the global LEI register",
                    "example": "2138002P5RNKC5W2JZ46"
                  },
                  "jurisdiction": {
                    "type": "string",
                    "default": "GB",
                    "description": "Only GB (Companies House) is currently supported; other values return status \"unverified\"",
                    "example": "GB"
                  },
                  "checks": {
                    "type": "object",
                    "properties": {
                      "sanctionsScreening": {
                        "type": "boolean",
                        "description": "Set false to skip sanctions and PEP list screening"
                      },
                      "leiLookup": {
                        "type": "boolean",
                        "description": "Set true to look up an LEI in the global LEI register by registered name"
                      }
                    }
                  },
                  "generateReport": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include a reportUrl (RPT-002 PDF) in the response"
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {}
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "KYB result — status reflects the actual registry outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "checkId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "passed",
                        "review",
                        "failed",
                        "unverified"
                      ]
                    },
                    "riskLevel": {
                      "type": "string",
                      "enum": [
                        "low",
                        "medium",
                        "high",
                        "unknown"
                      ],
                      "description": "\"unknown\" when the company could not be verified"
                    },
                    "riskScore": {
                      "type": "number",
                      "description": "0–100 composite (registry + screening); present when registry-verified"
                    },
                    "reason": {
                      "type": "string",
                      "description": "Machine-readable cause for non-passed statuses, e.g. company_not_found, ambiguous_name_match, registry_unavailable, aml_match"
                    },
                    "jurisdiction": {
                      "type": "string"
                    },
                    "companyNumber": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "companyName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "company": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "companyNumber": {
                          "type": "string"
                        },
                        "companyName": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "incorporatedOn": {
                          "type": "string"
                        },
                        "registeredOffice": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": {
                            "type": "string"
                          }
                        },
                        "sicCodes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "officerCount": {
                          "type": "number"
                        },
                        "officers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "role": {
                                "type": "string"
                              },
                              "appointedOn": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "nationality": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            },
                            "required": [
                              "name",
                              "role",
                              "appointedOn",
                              "nationality"
                            ]
                          }
                        },
                        "pscCount": {
                          "type": "number"
                        },
                        "pscs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "ownershipBand": {
                                "type": "string",
                                "description": "Companies House band, e.g. \"25-50%\" or \"more than 75%\". Present when owner screening is enabled on your account.",
                                "example": "25-50%"
                              },
                              "natureOfControl": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "nationality": {
                                "type": "string"
                              },
                              "kind": {
                                "type": "string",
                                "enum": [
                                  "person",
                                  "organization"
                                ]
                              },
                              "sanctions": {
                                "type": "string",
                                "enum": [
                                  "Clear",
                                  "Review",
                                  "Match",
                                  "Not screened"
                                ],
                                "description": "Owner sanctions result. \"Not screened\" means the screen did not run. Present when owner screening is enabled on your account."
                              }
                            },
                            "required": [
                              "name"
                            ]
                          },
                          "description": "PSC bands and nature-of-control codes. Omitted when owner screening is not enabled on your account."
                        },
                        "insolvent": {
                          "type": "boolean"
                        },
                        "registryRiskScore": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "companyNumber",
                        "companyName",
                        "status",
                        "type",
                        "incorporatedOn",
                        "registeredOffice",
                        "sicCodes",
                        "officerCount",
                        "officers",
                        "pscCount",
                        "insolvent",
                        "registryRiskScore"
                      ],
                      "description": "Registered company data; null unless registry-verified"
                    },
                    "registry": {
                      "type": "object",
                      "properties": {
                        "source": {
                          "type": "string",
                          "enum": [
                            "companies_house"
                          ]
                        },
                        "verified": {
                          "type": "boolean"
                        },
                        "available": {
                          "type": "boolean"
                        },
                        "matchedBy": {
                          "type": "string",
                          "enum": [
                            "number",
                            "name"
                          ]
                        },
                        "nameSimilarity": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "source",
                        "verified",
                        "available"
                      ]
                    },
                    "candidates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "companyNumber": {
                            "type": "string"
                          },
                          "companyName": {
                            "type": "string"
                          },
                          "companyStatus": {
                            "type": "string"
                          },
                          "similarity": {
                            "type": "number"
                          }
                        },
                        "required": [
                          "companyNumber",
                          "companyName",
                          "companyStatus",
                          "similarity"
                        ]
                      },
                      "description": "\"Did you mean\" list when a name search had no sufficiently similar match"
                    },
                    "aml": {
                      "type": "object",
                      "properties": {
                        "clear": {
                          "type": [
                            "boolean",
                            "null"
                          ],
                          "description": "null when screening did not run"
                        },
                        "riskLevel": {
                          "type": "string",
                          "description": "clear | review | hit | not_screened"
                        },
                        "companyMatch": {
                          "type": "boolean"
                        },
                        "officerMatches": {
                          "type": "number"
                        },
                        "discardedWeakHits": {
                          "type": "number",
                          "description": "Low-score / wrong-entity-type candidates excluded by material-hit scoring"
                        },
                        "pscScreened": {
                          "type": "boolean",
                          "description": "Whether every PSC was screened. Present only when owner screening is enabled on your account."
                        },
                        "pscServiceAvailable": {
                          "type": "boolean"
                        },
                        "pscMatches": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "clear",
                        "riskLevel"
                      ]
                    },
                    "completedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "reportUrl": {
                      "type": "string"
                    },
                    "callback": {
                      "type": "string",
                      "enum": [
                        "not_sent"
                      ],
                      "description": "Present when the request carried a webhookUrl: per-request callbacks are paused and nothing was sent. The result is in this response."
                    },
                    "callbackReason": {
                      "type": "string",
                      "example": "Callbacks to a per-request webhookUrl are paused and nothing was sent. The result is in this response."
                    }
                  },
                  "required": [
                    "checkId",
                    "status",
                    "riskLevel",
                    "jurisdiction",
                    "companyNumber",
                    "companyName",
                    "company",
                    "registry",
                    "aml",
                    "completedAt"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "companyNumber or companyName is required"
          },
          "401": {
            "description": "Invalid API key"
          }
        },
        "operationId": "postKybVerify"
      }
    },
    "/api/v1/aml/screen": {
      "post": {
        "tags": [
          "AML"
        ],
        "summary": "Screen an individual or business for AML/sanctions (API key)",
        "description": "Screens the subject against sanctions and PEP list. A 200 means the screen ran: `riskLevel` and `hits` describe what was found, and `pepStatus` / `adverseMedia` are derived from the matched entities' topics. If sanctions and PEP list does not return a result the endpoint responds 503 — it never reports an unscreened subject as clear, because a clear that was never performed is not evidence and cannot support a CDD record.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "individual",
                      "business"
                    ],
                    "default": "individual",
                    "description": "What the subject is: \"individual\", \"person\" for a person, matched against people; \"business\", \"company\", \"organisation\", \"organization\", \"entity\" for an organisation, matched against organisations. Case-insensitive. Omit it to screen a person. Any other value is a 400 and nothing is screened.",
                    "example": "individual"
                  },
                  "name": {
                    "type": "string",
                    "example": "Jane Smith"
                  },
                  "dateOfBirth": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "AML screening result — the screen ran",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AmlResult"
                }
              }
            }
          },
          "400": {
            "description": "validation_failed — name is required, or `type` is not an accepted value (nothing was screened)"
          },
          "401": {
            "description": "Invalid API key"
          },
          "503": {
            "description": "Screening could not be completed (sanctions and PEP list unavailable). No result is returned. This is not a clear and must not be recorded as one. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "screening_unavailable"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "providers": {
                      "$ref": "#/components/schemas/ProviderStatusMap"
                    },
                    "requestId": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error",
                    "message",
                    "providers",
                    "requestId"
                  ]
                }
              }
            }
          }
        },
        "operationId": "postAmlScreen"
      }
    },
    "/api/v1/checks/{checkId}": {
      "get": {
        "tags": [
          "Checks"
        ],
        "summary": "Retrieve a check result by ID (API key)",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "checkId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Check result"
          },
          "401": {
            "description": "Invalid API key"
          },
          "404": {
            "description": "Check not found"
          }
        },
        "operationId": "getChecksByCheckId"
      }
    },
    "/api/v1/workflows/run": {
      "post": {
        "tags": [
          "Workflows"
        ],
        "summary": "Trigger a workflow by ID (API key)",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "workflowId": {
                    "type": "number",
                    "description": "Numeric id of one of your saved workflows",
                    "example": 12
                  },
                  "inputs": {
                    "type": "object",
                    "additionalProperties": {},
                    "description": "Execution context (userId, name, companyNumber, …). An aml step reads name, dateOfBirth, country and entityType; entityType takes the same values as POST /api/v1/aml/screen `type` (omit it to screen a person; \"business\" or \"company\" screens an organisation), and any other value is a 400. So is a type sent as `type` or `subjectType` without entityType."
                  },
                  "webhookUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Not called: per-request callbacks are paused, and the response carries callback \"not_sent\". The run result is in the response."
                  }
                },
                "required": [
                  "workflowId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Workflow execution result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "executionId": {
                      "type": "string"
                    },
                    "workflowId": {
                      "type": "number"
                    },
                    "workflowName": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed",
                        "incomplete",
                        "failed"
                      ]
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": {}
                      }
                    },
                    "executedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "callback": {
                      "type": "string",
                      "enum": [
                        "not_sent"
                      ],
                      "description": "Present when the request carried a webhookUrl: per-request callbacks are paused and nothing was sent. The result is in this response."
                    },
                    "callbackReason": {
                      "type": "string",
                      "example": "Callbacks to a per-request webhookUrl are paused and nothing was sent. The result is in this response."
                    }
                  },
                  "required": [
                    "executionId",
                    "workflowId",
                    "workflowName",
                    "status",
                    "results",
                    "executedAt"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "workflowId (numeric) is required / workflow inactive / an input a step reads is invalid (a non-numeric userId or transactionId, an unaccepted entityType, or an aml subject type sent as type or subjectType instead of entityType)"
          },
          "401": {
            "description": "Invalid API key"
          },
          "404": {
            "description": "Workflow not found"
          }
        },
        "operationId": "postWorkflowsRun"
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List registered webhook endpoints (API key)",
        "description": "Lists the endpoints on this account. Any key, test or live, can list. The signing secret is never returned here: it is shown once, in the response that creates the endpoint.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook endpoints (no signing secret)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookEndpoint"
                  }
                }
              }
            }
          },
          "500": {
            "description": "internal_error — the endpoints could not be read. Never answered as an empty list."
          }
        },
        "operationId": "getWebhooks"
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook endpoint (API key)",
        "description": "Registers an endpoint. Needs a live key. The 201 response is the only one that carries the whsec_… signing secret: store it then. No later response returns it.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Production listener"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://your-server.com/webhooks/tv"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "example": [
                      "aml.hit"
                    ]
                  }
                },
                "required": [
                  "name",
                  "url",
                  "events"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook registered — the response includes the whsec_… signing secret, shown this once",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/WebhookEndpoint"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "secret": {
                          "type": "string",
                          "description": "whsec_… signing secret. Shown once; never returned again."
                        }
                      },
                      "required": [
                        "secret"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Validation error"
          },
          "403": {
            "description": "test_key_cannot_manage_webhooks — endpoints receive production events, so creating, changing or deleting one needs a live key (tv_live_sk_…). A test key can list them."
          }
        },
        "operationId": "postWebhooks"
      }
    },
    "/api/v1/webhooks/{id}": {
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook endpoint (API key)",
        "description": "Partial update of an owned endpoint — send only the fields to change. A changed url is re-screened against SSRF rules before being saved. Needs a live key. The response does not include the signing secret.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": [
                "number",
                "null"
              ]
            },
            "required": false,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "example": "Production listener"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://your-server.com/webhooks/tv"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "example": [
                      "aml.hit"
                    ]
                  },
                  "isActive": {
                    "type": "boolean",
                    "example": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated webhook (no signing secret)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "description": "validation_failed — non-numeric id, unsafe webhook URL, or invalid fields"
          },
          "403": {
            "description": "test_key_cannot_manage_webhooks — endpoints receive production events, so creating, changing or deleting one needs a live key (tv_live_sk_…). A test key can list them."
          },
          "404": {
            "description": "not_found — webhook does not exist or belongs to another account"
          }
        },
        "operationId": "patchWebhooksById"
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook endpoint (API key)",
        "description": "Deletes an owned endpoint. Needs a live key.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": [
                "number",
                "null"
              ]
            },
            "required": false,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook deleted"
          },
          "403": {
            "description": "test_key_cannot_manage_webhooks — endpoints receive production events, so creating, changing or deleting one needs a live key (tv_live_sk_…). A test key can list them."
          },
          "404": {
            "description": "Webhook not found for this account"
          }
        },
        "operationId": "deleteWebhooksById"
      }
    },
    "/api/v1/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Exchange an API key for a short-lived Bearer access token",
        "description": "client_credentials grant: clientSecret is your tv_*_sk_… API key. The returned JWT authenticates against every /api/v1 endpoint via Authorization: Bearer for one hour.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grantType": {
                    "type": "string",
                    "enum": [
                      "client_credentials"
                    ],
                    "example": "client_credentials"
                  },
                  "clientId": {
                    "type": "string",
                    "example": "my-integration"
                  },
                  "clientSecret": {
                    "type": "string",
                    "description": "Your tv_live_sk_… / tv_test_sk_… API key"
                  }
                },
                "required": [
                  "grantType",
                  "clientSecret"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Access token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accessToken": {
                      "type": "string"
                    },
                    "expiresIn": {
                      "type": "number",
                      "example": 3600
                    },
                    "tokenType": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    }
                  },
                  "required": [
                    "accessToken",
                    "expiresIn",
                    "tokenType"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Unsupported grant type or missing clientSecret"
          },
          "401": {
            "description": "Invalid client credentials"
          }
        },
        "operationId": "postToken"
      }
    },
    "/api/v1/validate-key": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "summary": "Validate the presented API key and return plan/usage info",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Key is valid",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "plan": {
                      "type": "string",
                      "example": "growth"
                    },
                    "tenantId": {
                      "type": "string"
                    },
                    "keyName": {
                      "type": "string"
                    },
                    "limits": {
                      "type": "object",
                      "properties": {
                        "requestsPerMinute": {
                          "type": "number",
                          "description": "The plan's advisory per-minute guidance; not enforced"
                        },
                        "requestsPerMonth": {
                          "type": "number",
                          "description": "The plan's monthly request allowance; -1 = no monthly allowance",
                          "example": 50000
                        }
                      },
                      "required": [
                        "requestsPerMinute",
                        "requestsPerMonth"
                      ]
                    },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "current": {
                          "type": "number"
                        },
                        "quota": {
                          "type": "number",
                          "description": "Same as limits.requestsPerMonth; -1 = no monthly allowance"
                        }
                      },
                      "required": [
                        "current",
                        "quota"
                      ]
                    },
                    "subscriptionStatus": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The payment processor's status for the account's paid subscription when it is waiting on a payment (past_due, unpaid, incomplete or paused), which is why `plan` is the starter allowance; null otherwise",
                      "example": "past_due"
                    },
                    "lapsedPlan": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The plan that subscription is for, set with subscriptionStatus; null otherwise",
                      "example": "growth"
                    }
                  },
                  "required": [
                    "valid",
                    "plan",
                    "tenantId",
                    "keyName",
                    "limits",
                    "usage",
                    "subscriptionStatus",
                    "lapsedPlan"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key"
          },
          "429": {
            "description": "The monthly request allowance of the key's plan is spent (the same 429 every /api/v1 request gets). When the account's paid subscription is waiting on a payment, the starter allowance applies and the body also carries code SUBSCRIPTION_NOT_ACTIVE, subscriptionStatus, a message and upgradeUrl.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "Monthly quota exceeded"
                      ]
                    },
                    "quota": {
                      "type": "number"
                    },
                    "usage": {
                      "type": "number"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "SUBSCRIPTION_NOT_ACTIVE"
                      ]
                    },
                    "subscriptionStatus": {
                      "type": "string",
                      "example": "past_due"
                    },
                    "message": {
                      "type": "string",
                      "example": "Your Growth subscription has a payment outstanding, so the starter allowance of 1,000 calls a month applies until it is paid."
                    },
                    "upgradeUrl": {
                      "type": "string",
                      "example": "/dashboard/billing"
                    }
                  },
                  "required": [
                    "error",
                    "quota",
                    "usage"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "The key is valid, but the plan of the account that owns it could not be read, so no plan or limits are reported. The monthly allowance is not enforced on a request whose plan is unknown. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "plan_unavailable"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "valid",
                    "error",
                    "message"
                  ]
                }
              }
            }
          }
        },
        "operationId": "getValidateKey"
      }
    },
    "/api/v1/aml/monitor": {
      "post": {
        "tags": [
          "AML"
        ],
        "summary": "Enrol a subject in ongoing AML monitoring",
        "description": "Screens the subject immediately and, if that screen succeeds, enrols them for daily re-screening. New matches are delivered as the `aml.hit` webhook event to endpoints registered with POST /api/v1/webhooks — exactly once per new match, keyed on the sanctions and PEP list entity id, so a changed caption or score does not re-alert. The enrolment screen is the baseline the daily diff runs against: if it cannot be completed the endpoint returns 503 and NO monitor is created, because a monitor with an unknown baseline would report every pre-existing listing as a new match. Metered per ACTIVE subject against the plan limit, not per screen — daily re-screens do not consume the monthly API quota. Requires the aml_monitoring feature (Growth and above). There is no per-monitor webhookUrl: supplying one is a 400.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entityId": {
                    "type": "string",
                    "description": "Your own reference for the subject. Re-POSTing an active one returns it unchanged.",
                    "example": "cust_88213"
                  },
                  "name": {
                    "type": "string",
                    "description": "The name actually screened",
                    "example": "Ivan Petrov"
                  },
                  "entityType": {
                    "type": "string",
                    "enum": [
                      "individual",
                      "business"
                    ],
                    "description": "What the subject is: \"individual\", \"person\" for a person, matched against people; \"business\", \"company\", \"organisation\", \"organization\", \"entity\" for an organisation, matched against organisations. Case-insensitive. Omit it to screen a person. Any other value is a 400 and nothing is screened.",
                    "example": "individual"
                  },
                  "dateOfBirth": {
                    "type": "string",
                    "description": "YYYY-MM-DD. Strongly recommended — narrows common-name false positives that would otherwise alert on every re-screen.",
                    "example": "1971-04-02"
                  },
                  "country": {
                    "type": "string",
                    "description": "ISO-3166 alpha-2",
                    "example": "RU"
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": {},
                    "example": {
                      "bookingRef": "B-4412"
                    }
                  }
                },
                "required": [
                  "entityId",
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An active monitor already existed for this entityId; returned unchanged, no new screen was run and no quota consumed"
          },
          "201": {
            "description": "Monitor created; the enrolment screen completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitorId": {
                      "type": "string",
                      "example": "MON_LX9K2AB7"
                    },
                    "entityId": {
                      "type": "string",
                      "description": "Your own reference for the subject",
                      "example": "cust_88213"
                    },
                    "entityType": {
                      "type": "string",
                      "enum": [
                        "individual",
                        "business"
                      ],
                      "description": "What the subject was screened as. \"business\" means it was matched against organisations.",
                      "example": "individual"
                    },
                    "name": {
                      "type": "string",
                      "example": "Ivan Petrov"
                    },
                    "dateOfBirth": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "1971-04-02"
                    },
                    "country": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "RU"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "suspended",
                        "cancelled"
                      ],
                      "example": "active"
                    },
                    "suspensionReason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "plan_lapsed or over_plan_limit when status is suspended",
                      "example": null
                    },
                    "checkFrequency": {
                      "type": "string",
                      "example": "daily"
                    },
                    "lastScreenedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "The last screen that actually COMPLETED. Never advanced by a failed attempt, so it bounds the window in which a new match could have appeared unnoticed."
                    },
                    "lastRiskLevel": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "clear"
                    },
                    "lastHitCount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "example": 0
                    },
                    "consecutiveFailures": {
                      "type": "number",
                      "description": "Consecutive failed screens. Non-zero means monitoring is currently degraded for this subject.",
                      "example": 0
                    },
                    "nextCheckAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "cancelledAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "createdAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "metadata": {
                      "type": "object",
                      "additionalProperties": {}
                    },
                    "environment": {
                      "type": "string",
                      "example": "production"
                    },
                    "baselineCheckId": {
                      "type": "string",
                      "description": "The enrolment screen, retrievable as a check and renderable as an RPT-003 report",
                      "example": "CHK_M2X9AB"
                    },
                    "baseline": {
                      "type": "object",
                      "properties": {
                        "hitCount": {
                          "type": "number"
                        },
                        "riskLevel": {
                          "type": "string"
                        },
                        "sanctionsMatch": {
                          "type": "boolean"
                        },
                        "pepStatus": {
                          "type": "boolean"
                        },
                        "adverseMedia": {
                          "type": "boolean"
                        },
                        "screenedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "required": [
                        "hitCount",
                        "riskLevel",
                        "sanctionsMatch",
                        "pepStatus",
                        "adverseMedia",
                        "screenedAt"
                      ],
                      "description": "What the subject already matched at enrolment. Never alerted on."
                    },
                    "alerting": {
                      "type": "object",
                      "properties": {
                        "event": {
                          "type": "string",
                          "enum": [
                            "aml.hit"
                          ]
                        },
                        "subscribedEndpoints": {
                          "type": "number",
                          "description": "Active webhook endpoints subscribed to aml.hit. Zero means new matches cannot be delivered."
                        },
                        "warning": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "event",
                        "subscribedEndpoints"
                      ]
                    },
                    "quota": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "null when the plan is unlimited"
                        },
                        "active": {
                          "type": "number"
                        },
                        "remaining": {
                          "type": [
                            "number",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "limit",
                        "active",
                        "remaining"
                      ]
                    }
                  },
                  "required": [
                    "monitorId",
                    "entityId",
                    "entityType",
                    "name",
                    "dateOfBirth",
                    "country",
                    "status",
                    "suspensionReason",
                    "checkFrequency",
                    "lastScreenedAt",
                    "lastRiskLevel",
                    "lastHitCount",
                    "consecutiveFailures",
                    "nextCheckAt",
                    "cancelledAt",
                    "createdAt",
                    "metadata",
                    "environment",
                    "baselineCheckId",
                    "baseline",
                    "alerting",
                    "quota"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "validation_failed — entityId/name missing, entityType not an accepted value, or a webhookUrl was supplied"
          },
          "403": {
            "description": "PLAN_UPGRADE_REQUIRED (plan has no ongoing monitoring) or QUOTA_EXCEEDED (active-subject limit reached)"
          },
          "501": {
            "description": "NOT_IMPLEMENTED — monitoring is not enabled on this deployment"
          },
          "503": {
            "description": "screening_unavailable — the enrolment screen could not be completed, so no monitor was created"
          }
        },
        "operationId": "postAmlMonitor"
      },
      "get": {
        "tags": [
          "AML"
        ],
        "summary": "List monitored subjects",
        "description": "Returns this account's monitors, newest first. `lastScreenedAt` and `consecutiveFailures` let you verify monitoring is actually running rather than taking it on trust.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "suspended",
                "cancelled"
              ]
            },
            "required": false,
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": false,
            "name": "entityId",
            "in": "query"
          },
          {
            "schema": {
              "type": "number",
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "number",
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Monitors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "monitorId": {
                            "type": "string",
                            "example": "MON_LX9K2AB7"
                          },
                          "entityId": {
                            "type": "string",
                            "description": "Your own reference for the subject",
                            "example": "cust_88213"
                          },
                          "entityType": {
                            "type": "string",
                            "enum": [
                              "individual",
                              "business"
                            ],
                            "description": "What the subject was screened as. \"business\" means it was matched against organisations.",
                            "example": "individual"
                          },
                          "name": {
                            "type": "string",
                            "example": "Ivan Petrov"
                          },
                          "dateOfBirth": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "example": "1971-04-02"
                          },
                          "country": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "example": "RU"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "suspended",
                              "cancelled"
                            ],
                            "example": "active"
                          },
                          "suspensionReason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "plan_lapsed or over_plan_limit when status is suspended",
                            "example": null
                          },
                          "checkFrequency": {
                            "type": "string",
                            "example": "daily"
                          },
                          "lastScreenedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "The last screen that actually COMPLETED. Never advanced by a failed attempt, so it bounds the window in which a new match could have appeared unnoticed."
                          },
                          "lastRiskLevel": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "example": "clear"
                          },
                          "lastHitCount": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "example": 0
                          },
                          "consecutiveFailures": {
                            "type": "number",
                            "description": "Consecutive failed screens. Non-zero means monitoring is currently degraded for this subject.",
                            "example": 0
                          },
                          "nextCheckAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "cancelledAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "createdAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "metadata": {
                            "type": "object",
                            "additionalProperties": {}
                          },
                          "environment": {
                            "type": "string",
                            "example": "production"
                          }
                        },
                        "required": [
                          "monitorId",
                          "entityId",
                          "entityType",
                          "name",
                          "dateOfBirth",
                          "country",
                          "status",
                          "suspensionReason",
                          "checkFrequency",
                          "lastScreenedAt",
                          "lastRiskLevel",
                          "lastHitCount",
                          "consecutiveFailures",
                          "nextCheckAt",
                          "cancelledAt",
                          "createdAt",
                          "metadata",
                          "environment"
                        ]
                      }
                    },
                    "total": {
                      "type": "number"
                    },
                    "limit": {
                      "type": "number"
                    },
                    "offset": {
                      "type": "number"
                    },
                    "quota": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": [
                            "number",
                            "null"
                          ]
                        },
                        "active": {
                          "type": "number"
                        },
                        "remaining": {
                          "type": [
                            "number",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "limit",
                        "active",
                        "remaining"
                      ]
                    }
                  },
                  "required": [
                    "monitors",
                    "total",
                    "limit",
                    "offset",
                    "quota"
                  ]
                }
              }
            }
          }
        },
        "operationId": "getAmlMonitor"
      }
    },
    "/api/v1/aml/monitor/{monitorId}": {
      "get": {
        "tags": [
          "AML"
        ],
        "summary": "Read one monitored subject and its match history",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "monitorId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Monitor with every match recorded for it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitorId": {
                      "type": "string",
                      "example": "MON_LX9K2AB7"
                    },
                    "entityId": {
                      "type": "string",
                      "description": "Your own reference for the subject",
                      "example": "cust_88213"
                    },
                    "entityType": {
                      "type": "string",
                      "enum": [
                        "individual",
                        "business"
                      ],
                      "description": "What the subject was screened as. \"business\" means it was matched against organisations.",
                      "example": "individual"
                    },
                    "name": {
                      "type": "string",
                      "example": "Ivan Petrov"
                    },
                    "dateOfBirth": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "1971-04-02"
                    },
                    "country": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "RU"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "suspended",
                        "cancelled"
                      ],
                      "example": "active"
                    },
                    "suspensionReason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "plan_lapsed or over_plan_limit when status is suspended",
                      "example": null
                    },
                    "checkFrequency": {
                      "type": "string",
                      "example": "daily"
                    },
                    "lastScreenedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "The last screen that actually COMPLETED. Never advanced by a failed attempt, so it bounds the window in which a new match could have appeared unnoticed."
                    },
                    "lastRiskLevel": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "clear"
                    },
                    "lastHitCount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "example": 0
                    },
                    "consecutiveFailures": {
                      "type": "number",
                      "description": "Consecutive failed screens. Non-zero means monitoring is currently degraded for this subject.",
                      "example": 0
                    },
                    "nextCheckAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "cancelledAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "createdAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "metadata": {
                      "type": "object",
                      "additionalProperties": {}
                    },
                    "environment": {
                      "type": "string",
                      "example": "production"
                    },
                    "hits": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "sanctions and PEP list entity id — the key new matches are diffed on"
                          },
                          "caption": {
                            "type": "string"
                          },
                          "score": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "schema": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "datasets": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "topics": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "properties": {
                            "type": "object",
                            "additionalProperties": {}
                          },
                          "isBaseline": {
                            "type": "boolean",
                            "description": "Present at enrolment, so never alerted on"
                          },
                          "detectedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "alertedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        },
                        "required": [
                          "id",
                          "caption",
                          "score",
                          "schema",
                          "datasets",
                          "topics",
                          "properties",
                          "isBaseline",
                          "detectedAt",
                          "alertedAt"
                        ]
                      }
                    },
                    "pendingAlerts": {
                      "type": "number",
                      "description": "New matches detected but not yet delivered"
                    }
                  },
                  "required": [
                    "monitorId",
                    "entityId",
                    "entityType",
                    "name",
                    "dateOfBirth",
                    "country",
                    "status",
                    "suspensionReason",
                    "checkFrequency",
                    "lastScreenedAt",
                    "lastRiskLevel",
                    "lastHitCount",
                    "consecutiveFailures",
                    "nextCheckAt",
                    "cancelledAt",
                    "createdAt",
                    "metadata",
                    "environment",
                    "hits",
                    "pendingAlerts"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not_found"
          }
        },
        "operationId": "getAmlMonitorByMonitorId"
      },
      "delete": {
        "tags": [
          "AML"
        ],
        "summary": "Cancel monitoring for a subject",
        "description": "Stops re-screening and frees the subject slot immediately. Returns 200 rather than 204 because the record is kept as evidence of what was monitored and when — it moves to status \"cancelled\" rather than being deleted. Idempotent. The cancelled record, and the sanctions matches recorded against it, are retained for the statutory period (5 years from cancellation under MLR 2017 reg. 40) and are then permanently deleted; after that the monitor id returns 404.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "monitorId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitorId": {
                      "type": "string",
                      "example": "MON_LX9K2AB7"
                    },
                    "entityId": {
                      "type": "string",
                      "description": "Your own reference for the subject",
                      "example": "cust_88213"
                    },
                    "entityType": {
                      "type": "string",
                      "enum": [
                        "individual",
                        "business"
                      ],
                      "description": "What the subject was screened as. \"business\" means it was matched against organisations.",
                      "example": "individual"
                    },
                    "name": {
                      "type": "string",
                      "example": "Ivan Petrov"
                    },
                    "dateOfBirth": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "1971-04-02"
                    },
                    "country": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "RU"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "suspended",
                        "cancelled"
                      ],
                      "example": "active"
                    },
                    "suspensionReason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "plan_lapsed or over_plan_limit when status is suspended",
                      "example": null
                    },
                    "checkFrequency": {
                      "type": "string",
                      "example": "daily"
                    },
                    "lastScreenedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "The last screen that actually COMPLETED. Never advanced by a failed attempt, so it bounds the window in which a new match could have appeared unnoticed."
                    },
                    "lastRiskLevel": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "clear"
                    },
                    "lastHitCount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "example": 0
                    },
                    "consecutiveFailures": {
                      "type": "number",
                      "description": "Consecutive failed screens. Non-zero means monitoring is currently degraded for this subject.",
                      "example": 0
                    },
                    "nextCheckAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "cancelledAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "createdAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "metadata": {
                      "type": "object",
                      "additionalProperties": {}
                    },
                    "environment": {
                      "type": "string",
                      "example": "production"
                    }
                  },
                  "required": [
                    "monitorId",
                    "entityId",
                    "entityType",
                    "name",
                    "dateOfBirth",
                    "country",
                    "status",
                    "suspensionReason",
                    "checkFrequency",
                    "lastScreenedAt",
                    "lastRiskLevel",
                    "lastHitCount",
                    "consecutiveFailures",
                    "nextCheckAt",
                    "cancelledAt",
                    "createdAt",
                    "metadata",
                    "environment"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "not_found"
          }
        },
        "operationId": "deleteAmlMonitorByMonitorId"
      }
    },
    "/api/v1/reports/{reportType}/{checkId}": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Report availability metadata / PDF download for a check",
        "description": "GET /api/v1/reports/RPT-001/{checkId} returns JSON availability metadata; GET /api/v1/reports/RPT-001/{checkId}.pdf streams the PDF report rendered from the stored check data. 404 when the check does not exist for your account.\n\n**Report types this API produces:** `RPT-001` Individual Identity Verification (KYC) — for `kyc` checks; `RPT-002` Business Verification (KYB) — for `kyb` checks; `RPT-003` AML Screening — for `aml_screen` checks. `RPT-016` is the ZIP evidence bundle and is requested via POST /api/v1/reports/rpt-016, not here.\n\nOther advertised report codes (RPT-004 … RPT-015) have no renderer on this API and return **501**; a code that is not a TrustVerify report type returns **400**; a valid code requested for a check of the wrong subject (for example RPT-002 for a `kyc` check) returns **409** with the correct type in `suggestedType`. No document is ever produced outside this set — a TrustVerify-branded PDF always means the named report was really rendered from the named check.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "enum": [
                "RPT-001",
                "RPT-002",
                "RPT-003"
              ],
              "example": "RPT-001"
            },
            "required": true,
            "name": "reportType",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "example": "CHK_ABC123 (append .pdf for the binary report)"
            },
            "required": true,
            "name": "checkId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Availability metadata (JSON) or the report itself (application/pdf with .pdf suffix)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reportType": {
                      "type": "string"
                    },
                    "reportTitle": {
                      "type": "string",
                      "example": "Individual Identity Verification (KYC) Report"
                    },
                    "checkId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "available"
                      ]
                    },
                    "downloadUrl": {
                      "type": "string"
                    },
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "retainedUntil": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "reportType",
                    "reportTitle",
                    "checkId",
                    "status",
                    "downloadUrl",
                    "generatedAt",
                    "retainedUntil"
                  ]
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Not a TrustVerify report type",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "validation_failed"
                      ]
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "UNKNOWN_REPORT_TYPE"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "supportedTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "error",
                    "code",
                    "message",
                    "supportedTypes"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Check not found — no report is available"
          },
          "409": {
            "description": "Report type does not match the subject of this check",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "report_type_mismatch"
                      ]
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "REPORT_TYPE_MISMATCH"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "checkType": {
                      "type": "string"
                    },
                    "suggestedType": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "supportedTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "error",
                    "code",
                    "message",
                    "checkType",
                    "suggestedType",
                    "supportedTypes"
                  ]
                }
              }
            }
          },
          "501": {
            "description": "Advertised report type with no renderer on this API — no document is produced",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "not_implemented"
                      ]
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "NOT_IMPLEMENTED"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "supportedTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "error",
                    "code",
                    "message",
                    "supportedTypes"
                  ]
                }
              }
            }
          }
        },
        "operationId": "getReportsByReportTypeByCheckId"
      }
    },
    "/api/v1/reports/rpt-016": {
      "post": {
        "tags": [
          "Reports"
        ],
        "summary": "Register a regulatory evidence bundle (RPT-016) for a check",
        "description": "Registers a ZIP evidence bundle for an existing check. `includeTypes` may only name report types this API produces for that check type (see GET /api/v1/reports/{reportType}/{checkId}); anything else is rejected with **400** and `unavailableTypes`, so a registered bundle can never promise a document the archive will not contain. Omit `includeTypes` to get the report for the check's own type (a `kyc` check → `[\"RPT-001\"]`).",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "checkId": {
                    "type": "string",
                    "description": "An existing check id from a previous /api/v1 call"
                  },
                  "includeTypes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Report types to render into the ZIP. Defaults to the report for the check's own type.",
                    "example": [
                      "RPT-001"
                    ]
                  },
                  "regulatorRef": {
                    "type": "string",
                    "example": "FCA-2026-1234"
                  }
                },
                "required": [
                  "checkId"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Bundle registered — poll the returned downloadUrl. `includeTypes` echoes what the ZIP will contain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bundleId": {
                      "type": "string"
                    },
                    "checkId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "generating"
                      ]
                    },
                    "includeTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "regulatorRef": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "estimatedReadyAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "downloadUrl": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "bundleId",
                    "checkId",
                    "status",
                    "includeTypes",
                    "regulatorRef",
                    "estimatedReadyAt",
                    "downloadUrl"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "checkId missing, or includeTypes names a report type this API cannot produce for this check",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "validation_failed"
                      ]
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "UNSUPPORTED_REPORT_TYPES",
                        "NO_REPORTS_FOR_CHECK_TYPE",
                        "EMPTY_INCLUDE_TYPES"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "unavailableTypes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "type",
                          "reason"
                        ]
                      }
                    },
                    "supportedTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "error",
                    "code",
                    "message",
                    "supportedTypes"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Check not found — a bundle can only be generated for a check that ran"
          }
        },
        "operationId": "postReportsRpt016"
      }
    },
    "/api/v1/bundles/{bundleId}": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Evidence bundle readiness metadata / ZIP download",
        "description": "GET /api/v1/bundles/{bundleId} returns JSON readiness metadata; GET /api/v1/bundles/{bundleId}.zip streams the ZIP. 404 when the bundle was never registered for your account.\n\n**Archive layout**\n\n```\n{bundleId}.zip\n├── report.pdf                       # bundle cover sheet (RPT-016)\n├── reports/RPT-001-{checkId}.pdf    # one PDF per included report type\n├── evidence.json                    # machine-readable check record + what is included\n└── manifest.json                    # sha256 + byte length of every other entry (not signed)\n```\n\n`manifest.json.files[]` covers every entry in the archive except `manifest.json` itself. Bundles are **not signed**: `signature` and `signatureAlgorithm` are null and `integrity` is `sha256_hashes_only`. Re-hashing each file against the manifest shows whether anything changed since you downloaded it; it does not prove who produced the archive. `manifest.json.unavailableReports[]` and `evidence.json.unavailableReports[]` list any requested type that was not produced, with the reason — a placeholder document is never substituted.",
        "security": [
          {
            "bearerApiKeyAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "bnd_CHK_ABC123 (append .zip for the binary bundle)"
            },
            "required": true,
            "name": "bundleId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Readiness metadata (JSON) or the bundle itself (application/zip with .zip suffix)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bundleId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "ready"
                      ]
                    },
                    "downloadUrl": {
                      "type": "string"
                    },
                    "signatureAlgorithm": {
                      "type": "null",
                      "description": "Always null: bundles are not signed. The manifest carries sha256 hashes only."
                    },
                    "integrity": {
                      "type": "string",
                      "enum": [
                        "sha256_hashes_only"
                      ]
                    },
                    "includeTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Report types the ZIP will really contain"
                    },
                    "unavailableReports": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "type",
                          "reason"
                        ]
                      }
                    },
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "bundleId",
                    "status",
                    "downloadUrl",
                    "signatureAlgorithm",
                    "integrity",
                    "includeTypes",
                    "unavailableReports",
                    "generatedAt"
                  ]
                }
              },
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "Bundle not found"
          }
        },
        "operationId": "getBundlesByBundleId"
      }
    }
  },
  "components": {
    "schemas": {
      "AmlResult": {
        "type": "object",
        "properties": {
          "subjectType": {
            "type": "string",
            "enum": [
              "individual",
              "business"
            ],
            "description": "What the subject was screened as. \"business\" means it was matched against organisations.",
            "example": "individual"
          },
          "status": {
            "type": "string",
            "enum": [
              "clear",
              "potential_match",
              "match"
            ],
            "example": "clear"
          },
          "riskLevel": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "hit"
            ],
            "example": "low"
          },
          "totalHits": {
            "type": "number",
            "example": 0
          },
          "sanctionsMatch": {
            "type": "boolean",
            "example": false
          },
          "pepStatus": {
            "type": "boolean",
            "description": "Derived from the matched entities' sanctions and PEP list topics (role.pep / role.rca). Only meaningful because the screen ran — an unavailable screen returns 503, not false.",
            "example": false
          },
          "adverseMedia": {
            "type": "boolean",
            "description": "Derived from matched-entity topics (crime*, poi, debarment, wanted). This endpoint performs NO media screening — no media provider is called — so this flag is a classification of sanctions-hit topics and must not be read as \"no adverse media exists\". See `coverage.adverseMedia`.",
            "example": false
          },
          "riskScore": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "example": 5
          },
          "hits": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": {}
            },
            "example": []
          }
        },
        "required": [
          "subjectType",
          "status",
          "riskLevel",
          "totalHits",
          "sanctionsMatch",
          "pepStatus",
          "adverseMedia",
          "riskScore",
          "hits"
        ]
      },
      "ProviderStatusMap": {
        "type": "object",
        "additionalProperties": {
          "type": "object",
          "properties": {
            "available": {
              "type": "boolean"
            },
            "status": {
              "type": "string",
              "enum": [
                "ok",
                "unavailable",
                "not_configured",
                "skipped"
              ]
            },
            "detail": {
              "type": "string"
            }
          },
          "required": [
            "available",
            "status"
          ]
        },
        "example": {}
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "number",
            "example": 3
          },
          "name": {
            "type": "string",
            "example": "Production listener"
          },
          "url": {
            "type": "string",
            "example": "https://your-server.com/webhooks/tv"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "aml.hit"
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "retryPolicy": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "maxRetries": {
                "type": "number"
              },
              "backoff": {
                "type": "string",
                "enum": [
                  "linear",
                  "exponential"
                ]
              }
            },
            "required": [
              "maxRetries",
              "backoff"
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "url",
          "events",
          "isActive",
          "retryPolicy",
          "createdAt",
          "updatedAt"
        ]
      }
    },
    "securitySchemes": {
      "bearerApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Primary auth for the Public API: `Authorization: Bearer tv_live_sk_…` / `tv_test_sk_…` (obtained from the Developer Portal), or a short-lived access token from POST /api/v1/token."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-TrustVerify-API-Key",
        "description": "Developer API key sent as a dedicated header — equivalent to Authorization: Bearer. If both headers are present, Authorization: Bearer wins."
      }
    }
  }
}
