openapi: 3.1.0
info:
  title: Zenoo API
  version: 2.0.0
  summary: Identity verification, KYB, KYC, and AML screening orchestration API
  description: |
    > 📘 **Welcome to the Zenoo API**
    >
    > Zenoo orchestrates identity verification, business verification, and compliance
    > screening across multiple providers through a single gateway API. Submit entity data,
    > Zenoo runs the configured checks, and you retrieve structured results via sync response,
    > pull token, or webhook.

    ## How It Works

    ```
    Your System ──► Zenoo Gateway ──► Company Registries
                        │              WorldCheck (PEP/Sanctions)
                        │              Document Verification
                        │              Biometric Liveness
                        ▼
                  Structured Results ──► Your System
    ```

    1. **Submit data** — Send company or individual data via the gateway API with your project hash
    2. **Zenoo orchestrates** — Checks run against configured providers (screening, registry, etc.)
    3. **Retrieve results** — Get structured compliance results via sync response, pull token, or webhook

    ## Flow Models

    | Model | Endpoint | Best For | Blocking |
    |-------|----------|----------|----------|
    | **Sync (Model 1)** | `POST /api` with `X-SYNC-TIMEOUT` | Server-to-server checks (screening, KYB) | Yes |
    | **Async (Model 2)** | `POST /init`, poll or await webhook | Client-facing journeys (KYC, document capture) | No |

    ## Tag Groups

    - **Verification** — KYB (business), KYC (individual), and AML Screening operations
    - **Results & Events** — Pull-based result retrieval and webhook event delivery

    ## Authentication

    All requests require an `X-API-KEY` header with your project-scoped API key.
    See the `ApiKeyAuth` security scheme for key management, rotation, and storage guidance.

    > 🚧 **API keys are project-scoped**
    >
    > Each project hash has its own API key. Staging and production use different keys.
    > Never expose API keys in client-side code.

    ## Key Schemas

    - **ErrorCode** — 10 machine-readable error codes with retry classification
    - **CheckTypeCode** — 17 verification check types across 4 categories
    - **Verdict** / **RiskTier** — Decision outputs for automated workflows
    - **ComplianceReport** — Full verification results with enrichments and risk scoring

    ## Versioning

    This API follows semantic versioning. The current version is **v2.0.0**. Breaking
    changes are communicated via email and the Zenoo changelog at least 30 days in advance.
  contact:
    name: Zenoo Developer Support
    email: support@zenoo.com
    url: https://zenoo.com
  license:
    name: Proprietary
    url: https://zenoo.com/terms

servers:
  - url: https://instance.prod.onboardapp.io
    description: Production
  - url: https://instance.staging.onboardapp.io
    description: Staging / Sandbox

security:
  - ApiKeyAuth: []

tags:
  - name: KYB - Business Verification
    description: |
      Verify companies against official registries, extract directors and UBOs,
      and screen corporate entities against PEP/sanctions databases.

      ## Capabilities

      | Check | Provider | What It Returns |
      |-------|----------|----------------|
      | Company registry | Registry provider | Legal name, registration number, status, directors, address |
      | Corporate screening | WorldCheck | PEP, sanctions, adverse media on the company entity |
      | Director screening | WorldCheck | Individual screening for each director |
      | UBO screening | WorldCheck | Individual screening for each beneficial owner |

      ## Lifecycle

      ```
      Submit ──► Registry Check ──► Director/UBO Extraction ──► Screening ──► Results
                  (Registry)        (from registry data)      (WorldCheck)
      ```

      > 📘 **Two execution models available**
      >
      > Use `POST /api` with `X-SYNC-TIMEOUT` for synchronous results, or
      > `POST /init` for async processing with webhook/polling delivery.

  - name: KYC - Individual Verification
    description: |
      Verify individuals through document checks, biometric liveness, database lookups,
      and phone/email verification.

      ## Capabilities

      | Check | Description | Typical Time |
      |-------|-------------|-------------|
      | Document capture | Passport, national ID, driving licence OCR | 10-30s (after user) |
      | Biometric liveness | Selfie + anti-spoofing detection | 5-15s (after user) |
      | Face match | Document photo vs selfie comparison | Included in biometric |
      | Database verification | Name/DOB/address validation | 3-10s |
      | PEP/sanctions screening | WorldCheck individual screening | 3-10s |

      ## Lifecycle

      ```
      Init Journey ──► User Redirected ──► Document + Selfie ──► Processing ──► Results
         (tokens)      (start token URL)    (user interaction)    (10-30s)     (webhook/pull)
      ```

      > 🚧 **KYC requires user interaction**
      >
      > KYC flows are typically **async (Model 2)** — you initiate a journey, redirect
      > the user to the verification URL, and receive results via webhook or pull token.
      > Use the sync `POST /kyc/api` endpoint only for non-interactive checks (database, phone).

  - name: AML Screening
    description: |
      Screen persons or companies against PEP, sanctions, adverse media, and watchlist
      databases. Screening runs synchronously (Model 1) and returns structured match
      results with confidence scores.

      ## Screening Categories

      | Category | Database | Match Types |
      |----------|----------|-------------|
      | **PEP** | WorldCheck | Class 1-4 political exposure, family, associates |
      | **Sanctions** | OFAC, EU, UN, HMT | SDN, consolidated lists, sectoral sanctions |
      | **Adverse Media** | Global news sources | Financial crime, fraud, terrorism, money laundering |
      | **Watchlist** | Law enforcement | Interpol, FBI, fugitive databases |

      > 📘 **Screening also runs within KYB/KYC**
      >
      > Screening is included automatically in KYB and KYC flows. Use the standalone
      > screening endpoint for ongoing monitoring, quick pre-checks, or batch screening.

  - name: Results
    description: |
      Retrieve verification results using a pull token obtained from an async journey
      initiation (`POST /init` or `POST /kyc/init`).

      ## Polling Strategy

      ```
      First 2 minutes  →  poll every 10 seconds
      After 2 minutes   →  poll every 30 seconds
      Maximum           →  10 minutes total
      ```

      > 📘 **Prefer webhooks over polling**
      >
      > Webhooks provide real-time delivery and reduce unnecessary API calls.
      > Use polling as a fallback. See [Webhooks Guide](/guides/webhooks).

  - name: Webhooks
    description: |
      Receive real-time notifications when verifications complete, journeys are abandoned,
      or checks fail. Webhook payloads are signed with HMAC-SHA256 for verification.

      ## Events

      | Event | Trigger | Typical Latency |
      |-------|---------|----------------|
      | `verification.completed` | KYC/KYB journey completes | <5 seconds |
      | `screening.completed` | Standalone screening finishes | <5 seconds |
      | `journey.abandoned` | User abandons interactive flow | 5-15 minutes |
      | `journey.expired` | Verification URL expires (24h) | At expiry time |
      | `check.failed` | Check fails after all retries | After last retry |

      ## Signature Verification

      Every webhook includes an `X-Zenoo-Signature` header. The value is `sha256=` followed
      by the hex-encoded HMAC-SHA256 digest of the raw request body, computed with your webhook secret.

      **Algorithm:**
      1. Extract the `X-Zenoo-Signature` header value.
      2. Remove the `sha256=` prefix to isolate the hex digest.
      3. Compute HMAC-SHA256 of the raw request body using your webhook secret.
      4. Compare using a constant-time comparison function. If they don't match, reject with `401`.

      **Node.js example:**
      ```javascript
      const crypto = require('crypto');

      function verifyWebhookSignature(rawBody, signature, secret) {
        const expected = crypto.createHmac('sha256', secret)
          .update(rawBody, 'utf8').digest('hex');
        const received = signature.replace('sha256=', '');
        return crypto.timingSafeEqual(
          Buffer.from(expected, 'hex'),
          Buffer.from(received, 'hex')
        );
      }
      ```

      **Constant-time comparison functions:**

      | Language | Function |
      |----------|----------|
      | Node.js | `crypto.timingSafeEqual()` |
      | Python | `hmac.compare_digest()` |
      | Java | `MessageDigest.isEqual()` |
      | Go | `hmac.Equal()` |

      > ❗ **Never use `==` or `.equals()`** for signature comparison — they are vulnerable to timing attacks.

      ## Retry Schedule

      If your endpoint does not return `200-299` within 30 seconds, Zenoo retries:

      | Attempt | Delay |
      |---------|-------|
      | 1st retry | 1 minute |
      | 2nd retry | 5 minutes |
      | 3rd retry | 30 minutes |
      | 4th retry | 1 hour |
      | 5th–168th | Every 1 hour for 7 days |

      After 7 days of consecutive failures, the webhook is marked as undeliverable.
      Contact Zenoo support to replay failed events.

      ## Idempotency

      Your webhook handler must be idempotent. Zenoo may deliver the same event more than once.
      Deduplicate using `journey_id` + `event_type` as a composite key. Always return `200`
      for duplicate events to prevent unnecessary retries.

      ## Best Practices

      - Return `200` immediately, then process the event asynchronously in a background job.
      - Verify `X-Zenoo-Signature` on every request before processing.
      - Use HTTPS for your webhook endpoint (HTTP is not supported).
      - Store the raw payload before processing so you can replay locally if needed.

x-tagGroups:
  - name: Verification
    tags:
      - KYB - Business Verification
      - KYC - Individual Verification
      - AML Screening
  - name: Results & Events
    tags:
      - Results
      - Webhooks

paths:
  /api/gateway/execute/{project_hash}/api:
    post:
      tags:
        - KYB - Business Verification
      operationId: verifyCompanySync
      summary: Verify Company (Sync)
      description: |
        Submit company data for synchronous KYB verification. The API blocks until all
        checks complete or the `X-SYNC-TIMEOUT` is reached, then returns a full compliance report.

        > 📘 **Prerequisites**
        >
        > - Valid `X-API-KEY` header with your project API key
        > - `X-SYNC-TIMEOUT` header (recommended: `30000` for simple, `60000` for multi-provider)
        > - Minimum required fields: `company_name`, `registration_number`, `country`

        ## What Happens

        1. Company is verified against the official registry
        2. Directors and UBOs are extracted from registry data
        3. PEP/sanctions/adverse media screening runs on all entities (WorldCheck)
        4. Risk model evaluates entity, geography, and screening results
        5. Structured compliance report is returned with verdict and risk tier

        ## Data Tiers

        | Tier | Fields | Result Quality |
        |------|--------|---------------|
        | **Minimum** | `company_name`, `registration_number`, `country` | Registry + company screening |
        | **Standard** | + `directors[]`, `ubos[]`, address | + Individual screening, correlation |
        | **Full** | + `external_reference`, revenue, SIC codes | + Idempotency, enriched risk scoring |

        > 🚧 **Timeout considerations**
        >
        > If the timeout is reached before all checks complete, you may receive partial results.
        > For complex multi-provider flows, use `60000` (60s) or consider the async `/init` endpoint.

        > 👍 **Success criteria**
        >
        > A successful response includes `overall_verdict` (Pass/Refer/Fail), `risk_tier`,
        > and `checks_summary` with per-check details. See the [KYB Quickstart](/quickstarts/kyb-quickstart).

        ## Decision Logic

        Combine `overall_verdict` and `risk_tier` from the response to determine the action:

        | Verdict | Risk Tier | Recommended Action |
        |---------|-----------|-------------------|
        | Pass | Low | Auto-approve. Proceed with onboarding. |
        | Pass | Medium | Auto-approve with enhanced monitoring. |
        | Refer | Low/Medium | Manual review by compliance analyst. |
        | Refer | High | Senior compliance review. Consider EDD. |
        | Fail | Any | Reject or request corrected information. |

        > ❗ **Sanctions halt rule:** If `sanctions_status` is `Hit` on any entity (company, director, or UBO),
        > immediately halt processing. Never auto-approve regardless of other check results.

        **Typical response time:** 10-30 seconds depending on jurisdiction and number of entities.
      parameters:
        - $ref: "#/components/parameters/ProjectHash"
        - name: X-SYNC-TIMEOUT
          in: header
          required: false
          description: |
            Timeout in milliseconds for synchronous execution. The API will block until
            checks complete or this timeout is reached. Recommended: `30000` (30 seconds)
            for simple checks, `60000` (60 seconds) for multi-provider flows.
          schema:
            type: integer
            default: 30000
            example: 30000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KybCompanyRequest"
            examples:
              minimum:
                summary: Minimum required fields
                value:
                  company_name: "Acme Holdings Ltd"
                  registration_number: "12345678"
                  country: "GB"
              standard:
                summary: Standard with directors and UBOs
                value:
                  company_name: "Acme Holdings Ltd"
                  registration_number: "12345678"
                  country: "GB"
                  company_type: "Private Limited Company (Ltd)"
                  incorporation_date: "2015-03-15"
                  industry: "Financial Services"
                  address_line_1: "123 High Street"
                  city: "London"
                  postcode: "EC1A 1BB"
                  directors:
                    - first_name: "Jane"
                      last_name: "Smith"
                      date_of_birth: "1980-05-20"
                      nationality: "GB"
                  ubos:
                    - first_name: "John"
                      last_name: "Doe"
                      date_of_birth: "1975-11-10"
                      nationality: "GB"
                      ownership_percentage: 75
              full:
                summary: Full payload with all fields
                value:
                  company_name: "Acme Holdings Ltd"
                  registration_number: "12345678"
                  country: "GB"
                  company_type: "Private Limited Company (Ltd)"
                  incorporation_date: "2015-03-15"
                  industry: "Financial Services"
                  address_line_1: "123 High Street"
                  city: "London"
                  postcode: "EC1A 1BB"
                  annual_revenue: "5000000"
                  employee_count: "50"
                  sic_codes: "64110,64999"
                  status: "Active"
                  external_reference: "LOAN-2026-0042"
                  directors:
                    - first_name: "Jane"
                      last_name: "Smith"
                      date_of_birth: "1980-05-20"
                      nationality: "GB"
                      role: "Director"
                      appointed_date: "2015-03-15"
                      zenoo_entity_id: "dir-001"
                  ubos:
                    - first_name: "John"
                      last_name: "Doe"
                      date_of_birth: "1975-11-10"
                      nationality: "GB"
                      ownership_percentage: 75
                      role: "UBO"
                      zenoo_entity_id: "ubo-001"
      responses:
        "200":
          description: Verification completed successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KybCompanyResponse"
              examples:
                CleanCompanyPass:
                  summary: Clean company — all checks pass
                  value:
                    company:
                      legal_name: "Acme Holdings Limited"
                      registration_number: "12345678"
                      jurisdiction: "GB"
                      company_status: "Active"
                      company_type: "Private Limited Company"
                      incorporation_date: "2015-03-15"
                      registered_address: "123 High Street, London, EC1A 1BB"
                      verification_status: "Verified"
                      verified_at: "2026-01-15T14:30:15Z"
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                      screening_completed_at: "2026-01-15T14:30:20Z"
                    directors:
                      - name: "Jane Smith"
                        date_of_birth: "1980-05-20"
                        role: "Director"
                        pep_status: "No Hit"
                        sanctions_status: "No Hit"
                    ubos:
                      - name: "John Doe"
                        date_of_birth: "1975-11-10"
                        nationality: "GB"
                        ownership_percentage: 75
                        pep_status: "No Hit"
                        sanctions_status: "No Hit"
                        identity_verified: false
                    checks:
                      registration:
                        provider: "Registry"
                        external_ref: "reg-ref-001"
                        result: "Pass"
                      directors:
                        provider: "Registry"
                        external_ref: "reg-ref-002"
                        result: "Pass"
                      screening:
                        provider: "WorldCheck"
                        external_ref: "wc-ref-001"
                        result: "Pass"
                SanctionsHitRefer:
                  summary: Director has PEP match — referred for review
                  value:
                    company:
                      legal_name: "Global Trade Corp"
                      registration_number: "87654321"
                      jurisdiction: "GB"
                      company_status: "Active"
                      verification_status: "Verified"
                      verified_at: "2026-01-15T14:30:15Z"
                    screening:
                      pep_status: "Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      adverse_media_count: 0
                      screening_provider: "WorldCheck"
                      screening_completed_at: "2026-01-15T14:30:20Z"
                    directors:
                      - name: "Robert Chen"
                        date_of_birth: "1968-09-12"
                        role: "Director"
                        pep_status: "Hit"
                        sanctions_status: "No Hit"
                        screening_completed_at: "2026-01-15T14:30:22Z"
                    checks:
                      registration:
                        provider: "Registry"
                        result: "Pass"
                      screening:
                        provider: "WorldCheck"
                        result: "Refer"
                RegistryNotFound:
                  summary: Company not found in registry
                  value:
                    company:
                      legal_name: "Unknown Corp"
                      registration_number: "INVALID999"
                      jurisdiction: "GB"
                      verification_status: "Failed"
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                    checks:
                      registration:
                        provider: "Registry"
                        result: "Fail"
                      screening:
                        provider: "WorldCheck"
                        result: "Pass"
                PartialTimeout:
                  summary: Some checks completed, others timed out
                  value:
                    company:
                      legal_name: "Acme Holdings Limited"
                      registration_number: "12345678"
                      jurisdiction: "GB"
                      company_status: "Active"
                      verification_status: "Verified"
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                    checks:
                      registration:
                        provider: "Registry"
                        result: "Pass"
                      directors:
                        provider: "Registry"
                        result: "Pass"
                      screening:
                        provider: "WorldCheck"
                        result: "Pass"
                      structure:
                        provider: "Registry"
                        result: "Refer"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/gateway/execute/{project_hash}/init:
    post:
      tags:
        - KYB - Business Verification
      operationId: initiateKybJourney
      summary: Initiate KYB Journey (Async)
      description: |
        Start an asynchronous KYB verification journey. Returns a token pair for
        result retrieval and optional interactive flow construction.

        > 📘 **When to use async KYB**
        >
        > Use this endpoint when you don't want to hold an HTTP connection open, when
        > processing multiple verifications concurrently, or when your timeout requirements
        > are shorter than typical KYB processing times.

        ## Token Pair

        | Token | Purpose | Lifetime |
        |-------|---------|----------|
        | **pull** | Retrieve results via `GET /sharable-payload/{pull_token}` | 30 days |
        | **start** | Construct verification URL: `https://{base}/{hash}/?t={start}` | 24 hours |

        ## Result Retrieval

        1. **Webhook** (recommended) — Configure a `verification.completed` webhook endpoint
        2. **Polling** — Call `GET /sharable-payload/{pull_token}` (200 = ready, 204 = processing)
        3. **Both** — Webhook for real-time, polling as fallback

        > 🚧 **Save both tokens**
        >
        > Store the pull token securely — you need it to retrieve results.
        > The start token is only needed if your flow requires interactive steps.

        **Typical processing time:** 30 seconds to 5 minutes depending on checks configured.
      parameters:
        - $ref: "#/components/parameters/ProjectHash"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KybCompanyRequest"
      responses:
        "200":
          description: Journey initiated — tokens returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
              examples:
                JourneyInitiated:
                  summary: New journey created successfully
                  value:
                    tokens:
                      pull: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.pull-token"
                      start: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.start-token"
                DuplicateJourney:
                  summary: Same external_reference returns existing tokens
                  value:
                    tokens:
                      pull: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.existing-pull"
                      start: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.existing-start"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/gateway/execute/{project_hash}/kyc/init:
    post:
      tags:
        - KYC - Individual Verification
      operationId: initiateKycJourney
      summary: Initiate KYC Journey
      description: |
        Start an identity verification journey for an individual. Returns a token pair
        used to construct the verification URL where the user completes document capture
        and biometric liveness checks.

        > 📘 **Prerequisites**
        >
        > - Valid `X-API-KEY` header
        > - Minimum required fields: `first_name`, `last_name`
        > - Recommended: `date_of_birth`, `country`, `email` for better match accuracy

        ## Verification Flow

        ```
        Your App                    Zenoo                      User
           │                          │                          │
           │── 1. POST /kyc/init ───►│                          │
           │◄── Tokens (pull+start) ──│                          │
           │                          │                          │
           │── 2. Redirect ─────────────────────────────────────►│
           │                          │◄── 3. Document + selfie ─│
           │                          │── 4. Process checks ────►│
           │◄── 5. Webhook ──────────│                          │
        ```

        ## Verification URL

        Construct the URL using the start token:
        ```
        https://{base_url}/{project_hash}/?t={start_token}
        ```

        > 🚧 **URL expiry**
        >
        > Verification URLs expire after **24 hours** by default. If the URL expires,
        > you'll receive a `journey.expired` webhook and must initiate a new journey.
        > Monitor the `journey.abandoned` webhook for users who start but don't finish.

        > 👍 **Mobile-ready**
        >
        > The verification UI is fully responsive. Open the URL in a WebView or system
        > browser on mobile devices — no SDK integration required.
      parameters:
        - $ref: "#/components/parameters/ProjectHash"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KycIndividualRequest"
            examples:
              minimum:
                summary: Minimum required fields
                value:
                  first_name: "Jane"
                  last_name: "Smith"
                  country: "GB"
              standard:
                summary: Standard KYC request
                value:
                  first_name: "Jane"
                  last_name: "Smith"
                  date_of_birth: "1990-06-15"
                  email: "jane.smith@example.com"
                  phone: "+447700900123"
                  country: "GB"
                  address: "42 Baker Street"
                  city: "London"
                  postal_code: "NW1 6XE"
              full:
                summary: Full payload with all fields
                value:
                  first_name: "Jane"
                  last_name: "Smith"
                  date_of_birth: "1990-06-15"
                  email: "jane.smith@example.com"
                  phone: "+447700900123"
                  country: "GB"
                  nationality: "GB"
                  address: "42 Baker Street"
                  city: "London"
                  postal_code: "NW1 6XE"
                  document_type: "passport"
                  external_reference: "APP-2026-0123"
      responses:
        "200":
          description: Journey initiated — tokens returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
              examples:
                JourneyInitiated:
                  summary: KYC journey created — redirect user to verification URL
                  value:
                    tokens:
                      pull: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJreWMtam91cm5leSJ9.pull-token"
                      start: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJreWMtam91cm5leSJ9.start-token"
                DuplicateJourney:
                  summary: Same external_reference — existing tokens returned (no duplicate)
                  value:
                    tokens:
                      pull: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJreWMtam91cm5leSJ9.existing-pull"
                      start: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJreWMtam91cm5leSJ9.existing-start"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/gateway/execute/{project_hash}/kyc/api:
    post:
      tags:
        - KYC - Individual Verification
      operationId: verifyIndividualSync
      summary: Verify Individual (Sync)
      description: |
        Run database and phone verification for an individual synchronously. Returns
        results directly in the response.

        > 📘 **Server-side checks only**
        >
        > Use this endpoint for checks that don't require user interaction: database
        > lookups, phone verification, email verification, and PEP/sanctions screening.
        > For document capture and biometric liveness, use the async
        > [Initiate KYC Journey](/reference/initiate-kyc-journey) endpoint instead.

        ## Supported Checks

        | Check | Requires | Typical Time |
        |-------|----------|-------------|
        | Database verification | `first_name`, `last_name`, `date_of_birth` | 3-10s |
        | Phone verification | `phone` (E.164) | 2-5s |
        | PEP/sanctions screening | `first_name`, `last_name` | 3-10s |

        > 🚧 **No document or biometric checks**
        >
        > This endpoint cannot perform document capture or biometric liveness — those
        > require user interaction via the async flow.
      parameters:
        - $ref: "#/components/parameters/ProjectHash"
        - name: X-SYNC-TIMEOUT
          in: header
          required: false
          schema:
            type: integer
            default: 30000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/KycIndividualRequest"
      responses:
        "200":
          description: Verification completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KycIndividualResponse"
              examples:
                CleanVerification:
                  summary: All checks pass — identity verified
                  value:
                    identity:
                      verified: true
                      decision: "Pass"
                      score: 95
                      authentication_id: "auth-xyz789"
                    document:
                      verified: true
                      document_type: "Passport"
                      document_country: "GB"
                      validation_result: "PASSED"
                      tamper_result: "Pass"
                      extracted_data:
                        full_name: "JANE ELIZABETH SMITH"
                        date_of_birth: "1990-06-15"
                        expiration_date: "2030-12-01"
                        gender: "F"
                    biometric:
                      liveness_verified: true
                      liveness_assessment: "Live"
                      liveness_score: 98.5
                      face_match: true
                      face_match_score: 96.2
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                DatabaseOnlyCheck:
                  summary: Server-side database + phone check (no document/biometric)
                  value:
                    identity:
                      verified: true
                      decision: "Pass"
                      score: 88
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                ScreeningHitRefer:
                  summary: Identity passes but PEP match found
                  value:
                    identity:
                      verified: true
                      decision: "Pass"
                      score: 92
                    screening:
                      pep_status: "Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                FullKycReport:
                  summary: Full KYC compliance report with all verification results
                  value:
                    case_reference: "AML-2026-0100"
                    external_reference: "APP-2026-0123"
                    overall_verdict: "Pass"
                    risk_tier: "Low"
                    identity:
                      verified: true
                      decision: "Pass"
                      score: 95
                      authentication_id: "auth-xyz789"
                    document:
                      verified: true
                      document_type: "Passport"
                      document_country: "GB"
                      validation_result: "PASSED"
                      tamper_result: "Pass"
                      extracted_data:
                        full_name: "JANE ELIZABETH SMITH"
                        date_of_birth: "1990-06-15"
                        expiration_date: "2030-12-01"
                        gender: "F"
                    biometric:
                      liveness_verified: true
                      liveness_assessment: "Live"
                      liveness_score: 98.5
                      face_match: true
                      face_match_score: 96.2
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "No Hit"
                      screening_provider: "WorldCheck"
                      screening_completed_at: "2026-01-15T14:35:10Z"
                    phone_verification:
                      verified: true
                      carrier: "Vodafone UK"
                      phone_type: "mobile"
                    email_verification:
                      verified: true
                      risk_score: 12
                      deliverable: true
                    address_verification:
                      verified: true
                      match_level: "full"
                    compliance_metadata:
                      cdd_completed: true
                      edd_required: false
                      reviewed_by: "system"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/gateway/execute/{project_hash}/screening/api:
    post:
      tags:
        - AML Screening
      operationId: screenEntity
      summary: Screen Entity
      description: |
        Screen a person or company against PEP, sanctions, adverse media, and watchlist
        databases. Runs synchronously via WorldCheck and returns structured match results.

        > 📘 **Prerequisites**
        >
        > - Valid `X-API-KEY` header
        > - Required field: `name`
        > - Recommended: `date_of_birth` (significantly reduces false positives for persons)

        ## Screening Categories

        | Category | Database | What It Finds |
        |----------|----------|--------------|
        | `pep` | WorldCheck | Politically Exposed Persons (current and former, classes 1-4) |
        | `sanctions` | OFAC, EU, UN, HMT | Sanctioned individuals and entities |
        | `adverse_media` | Global news sources | Financial crime, fraud, terrorism, money laundering |
        | `watchlist` | Law enforcement | Interpol, FBI, fugitive databases |

        ## Match Scoring

        Each match includes a confidence score (0-100):

        | Score | Confidence | Recommended Action |
        |-------|-----------|-------------------|
        | 90-100 | Very high | Investigate immediately |
        | 80-89 | High | Manual review required |
        | 70-79 | Medium | Review recommended |
        | Below 70 | Low | Likely false positive |

        > 🚧 **Screening within KYB/KYC**
        >
        > Screening runs automatically as part of KYB and KYC flows. Use this standalone
        > endpoint for ongoing monitoring, quick pre-checks, or batch screening.

        > 👍 **Reduce false positives**
        >
        > Always provide `date_of_birth` for person screening. This is the single most
        > effective field for reducing false positives on common names.

        ## Ongoing Monitoring

        Use this endpoint for periodic re-screening of existing customers. Recommended
        re-screening intervals by risk tier:

        | Risk Tier | Interval | Rationale |
        |-----------|----------|-----------|
        | High | Every 6 months | Highest scrutiny. Frequent sanctions/PEP database updates. |
        | Medium | Every 12 months | Standard CDD cycle. Aligns with most regulatory frameworks. |
        | Low | Every 24 months | Reduced frequency for low-risk customers with stable profiles. |

        New matches found during re-screening are compared against previous results.
        Only genuinely new matches generate compliance alerts.
      parameters:
        - $ref: "#/components/parameters/ProjectHash"
        - name: X-SYNC-TIMEOUT
          in: header
          required: false
          schema:
            type: integer
            default: 30000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScreeningRequest"
            examples:
              person:
                summary: Screen an individual
                value:
                  name: "John Smith"
                  date_of_birth: "1975-03-20"
                  country: "GB"
                  entity_type: "person"
                  categories:
                    - "pep"
                    - "sanctions"
                    - "adverse_media"
              company:
                summary: Screen a company
                value:
                  name: "Acme Holdings Ltd"
                  country: "GB"
                  entity_type: "company"
                  categories:
                    - "sanctions"
                    - "adverse_media"
      responses:
        "200":
          description: Screening completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScreeningResponse"
              examples:
                CleanNoHits:
                  summary: All categories clear — no matches found
                  value:
                    pep_status: "No Hit"
                    sanctions_status: "No Hit"
                    adverse_media_status: "No Hit"
                    screening_provider: "WorldCheck"
                    screening_completed_at: "2026-01-15T14:30:00Z"
                    matches: []
                PepHitFound:
                  summary: PEP match found — Class 2 political exposure
                  value:
                    pep_status: "Hit"
                    sanctions_status: "No Hit"
                    adverse_media_status: "No Hit"
                    screening_provider: "WorldCheck"
                    screening_completed_at: "2026-01-15T14:30:00Z"
                    matches:
                      - matchScore: 95.0
                        primaryName: "John Andrew Smith"
                        category: "pep"
                        categories:
                          - "pep-class-2"
                        types:
                          - "pep-class-2"
                        nationality: "British"
                        dateOfBirth: "1975-03-20"
                        sources:
                          - "UK Parliament"
                        positions:
                          - title: "Member of Parliament"
                            country: "United Kingdom"
                            from: "2019-12-12"
                            to: "2024-07-04"
                        associates:
                          - name: "Mary Smith"
                            association: "Spouse"
                            type: "family"
                MultipleMatches:
                  summary: Multiple screening hits across categories
                  value:
                    pep_status: "No Hit"
                    sanctions_status: "Hit"
                    adverse_media_status: "Hit"
                    screening_provider: "WorldCheck"
                    screening_completed_at: "2026-01-15T14:30:00Z"
                    matches:
                      - matchScore: 98.0
                        primaryName: "John Smith"
                        category: "sanctions"
                        categories:
                          - "sanction-sdn"
                        types:
                          - "sanction-sdn"
                        nationality: "British"
                        dateOfBirth: "1975-03-20"
                        sources:
                          - "OFAC SDN"
                      - matchScore: 92.5
                        primaryName: "John Smith"
                        category: "adverse_media"
                        categories:
                          - "adverse-media-financial-crime"
                        types:
                          - "adverse-media"
                        nationality: "British"
                        dateOfBirth: "1975-03-20"
                        sources:
                          - "Financial Times"
                          - "Reuters"
                        media:
                          - title: "Investigation into financial irregularities"
                            url: "https://example.com/article"
                            date: "2025-06-15"
                      - matchScore: 78.5
                        primaryName: "J. Smith"
                        category: "adverse_media"
                        categories:
                          - "adverse-media-fraud"
                        types:
                          - "adverse-media"
                        sources:
                          - "Bloomberg"
                CompanyScreening:
                  summary: Company entity screening — sanctions clear
                  value:
                    pep_status: "No Hit"
                    sanctions_status: "No Hit"
                    adverse_media_status: "Hit"
                    screening_provider: "WorldCheck"
                    screening_completed_at: "2026-01-15T14:30:00Z"
                    matches:
                      - matchScore: 87.0
                        primaryName: "Acme Holdings"
                        category: "adverse_media"
                        categories:
                          - "adverse-media-regulatory"
                        types:
                          - "adverse-media"
                        media:
                          - title: "Regulatory fine for compliance failures"
                            url: "https://example.com/regulatory-action"
                            date: "2025-09-20"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /api/gateway/sharable-payload/{pull_token}:
    get:
      tags:
        - Results
      operationId: getResults
      summary: Get Results
      description: |
        Retrieve verification results using a pull token from an async journey
        (`POST /init` or `POST /kyc/init`).

        > 📘 **Response codes**
        >
        > | Status | Meaning | Action |
        > |--------|---------|--------|
        > | `200` | Results ready | Parse JSON response |
        > | `204` | Still processing | Retry after delay |
        > | `404` | Invalid/expired token | Check token, initiate new journey |

        ## Polling Strategy

        ```
        First 2 minutes  →  poll every 10 seconds
        After 2 minutes   →  poll every 30 seconds
        Maximum           →  10 minutes total polling
        ```

        > 👍 **Prefer webhooks**
        >
        > Configure a `verification.completed` webhook for real-time delivery instead
        > of polling. Use polling as a fallback. See [Webhooks Guide](/guides/webhooks).

        > 🚧 **Token lifetime**
        >
        > Pull tokens remain valid for **30 days** after results become available.
        > After 30 days, the token expires and results must be retrieved through the
        > Zenoo dashboard or API case export.

        **No authentication required** — the pull token itself serves as the access credential.
      parameters:
        - name: pull_token
          in: path
          required: true
          description: The pull token received from a journey initiation call
          schema:
            type: string
      responses:
        "200":
          description: Results available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ComplianceReport"
              examples:
                CompleteKybResults:
                  summary: Full KYB compliance report — all checks complete
                  value:
                    case_reference: "AML-2026-0042"
                    external_reference: "LOAN-2026-0042"
                    processing_status: "complete"
                    overall_verdict: "Refer"
                    risk_tier: "Medium"
                    completed_at: "2026-01-15T14:32:00Z"
                    next_review_date: "2027-01-15"
                    company:
                      legal_name: "Acme Holdings Limited"
                      registration_number: "12345678"
                      jurisdiction: "GB"
                      company_status: "Active"
                      company_type: "Private Limited Company"
                      incorporation_date: "2015-03-15"
                      registered_address: "123 High Street, London, EC1A 1BB"
                      verification_status: "Verified"
                      verified_at: "2026-01-15T14:30:15Z"
                    screening:
                      pep_status: "No Hit"
                      sanctions_status: "No Hit"
                      adverse_media_status: "Hit"
                      adverse_media_count: 2
                      screening_provider: "WorldCheck"
                      screening_completed_at: "2026-01-15T14:30:20Z"
                    beneficial_owners:
                      - name: "John Doe"
                        date_of_birth: "1975-11-10"
                        nationality: "GB"
                        ownership_percentage: 75
                        pep_status: "No Hit"
                        sanctions_status: "No Hit"
                        identity_verified: false
                    directors:
                      - name: "Jane Smith"
                        date_of_birth: "1980-05-20"
                        role: "Director"
                        pep_status: "No Hit"
                        sanctions_status: "No Hit"
                    risk_assessment:
                      overall_score: 45
                      tier: "Medium"
                      factors:
                        - factor: "Adverse Media"
                          severity: "Medium"
                          detail: "2 adverse media articles found for company"
                    checks_summary:
                      total: 5
                      passed: 4
                      referred: 1
                      failed: 0
                      checks:
                        - type: "Company Registration"
                          status: "Pass"
                          provider: "Registry"
                          completed_at: "2026-01-15T14:30:15Z"
                        - type: "Company Screening"
                          status: "Refer"
                          provider: "WorldCheck"
                          matches: 2
                          completed_at: "2026-01-15T14:30:20Z"
                        - type: "Director Screening (Jane Smith)"
                          status: "Pass"
                          provider: "WorldCheck"
                        - type: "UBO Screening (John Doe)"
                          status: "Pass"
                          provider: "WorldCheck"
                        - type: "Directors Verification"
                          status: "Pass"
                          provider: "Registry"
                    compliance_metadata:
                      cdd_completed: true
                      cdd_completed_at: "2026-01-15T14:32:00Z"
                      edd_required: false
                      reviewed_by: "system"
                CompleteKycResults:
                  summary: Full KYC verification — identity verified
                  value:
                    case_reference: "AML-2026-0100"
                    external_reference: "APP-2026-0123"
                    processing_status: "complete"
                    overall_verdict: "Pass"
                    risk_tier: "Low"
                    completed_at: "2026-01-15T14:35:00Z"
                    compliance_metadata:
                      cdd_completed: true
                      reviewed_by: "system"
                ProcessingFailed:
                  summary: Verification failed — provider error
                  value:
                    case_reference: "AML-2026-0055"
                    external_reference: "LOAN-2026-0099"
                    processing_status: "failed"
                    overall_verdict: "Fail"
                    risk_tier: "High"
        "204":
          description: Processing not yet complete — retry later
        "404":
          description: Invalid or expired pull token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                TokenExpired:
                  summary: Pull token has expired (30-day lifetime)
                  value:
                    error: "TOKEN_EXPIRED"
                    message: "Pull token has expired. Results are available through the Zenoo dashboard."
                    request_id: "req-e5f6g7h8"
                InvalidToken:
                  summary: Pull token is malformed or does not exist
                  value:
                    error: "NOT_FOUND"
                    message: "Invalid pull token"
                    request_id: "req-i9j0k1l2"

webhooks:
  verification.completed:
    post:
      tags:
        - Webhooks
      operationId: webhookVerificationCompleted
      summary: Verification Completed
      description: |
        Fired when a KYC or KYB verification journey completes with results.
        This is the primary webhook for processing verification outcomes.

        > 📘 **Trigger conditions**
        >
        > - All configured checks have completed (passed, referred, or failed)
        > - The compliance report is ready for consumption
        > - Fires for both sync and async flows (if webhook is configured)

        ## Correlation

        Use `callback_reference` to match this event to your original request's
        `external_reference`. The `journey_id` is Zenoo's internal identifier.

        > ❗ **Always verify signatures**
        >
        > Validate the `X-Zenoo-Signature` header using HMAC-SHA256 with your
        > webhook secret before processing. See [Authentication](/getting-started/authentication).

        > 🚧 **Idempotency**
        >
        > This event may be delivered more than once. Deduplicate using `journey_id` +
        > `event_type`. Always return `200 OK` even for duplicates to prevent retries.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerificationCompletedEvent"
      responses:
        "200":
          description: Acknowledged

  screening.completed:
    post:
      tags:
        - Webhooks
      operationId: webhookScreeningCompleted
      summary: Screening Completed
      description: |
        Fired when standalone PEP/sanctions/adverse media screening completes for an entity.
        Includes the entity name, type, and screening results summary.

        > 📘 **Standalone screening only**
        >
        > This event fires for calls to the [Screen Entity](/reference/screen-entity)
        > endpoint. Screening that runs as part of KYB/KYC flows is included in the
        > `verification.completed` event instead.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScreeningCompletedEvent"
      responses:
        "200":
          description: Acknowledged

  journey.abandoned:
    post:
      tags:
        - Webhooks
      operationId: webhookJourneyAbandoned
      summary: Journey Abandoned
      description: |
        Fired when a user abandons an interactive verification journey
        (closes the browser, navigates away, or times out during a step).

        > 🚧 **Detection delay**
        >
        > Abandonment is detected after a period of inactivity (typically 5-15 minutes).
        > The exact timing depends on where the user dropped off in the flow.

        **Recommended action:** Send a reminder email or SMS with a new verification link.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JourneyAbandonedEvent"
      responses:
        "200":
          description: Acknowledged

  journey.expired:
    post:
      tags:
        - Webhooks
      operationId: webhookJourneyExpired
      summary: Journey Expired
      description: |
        Fired when a verification journey URL expires without the user completing
        the flow. Default expiry is 24 hours from journey creation.

        > 📘 **Expiry is per-journey**
        >
        > Each journey initiation creates a new 24-hour window. Initiating a new
        > journey for the same `external_reference` does not extend the original.

        **Recommended action:** Initiate a new journey and send the user a fresh verification link.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JourneyExpiredEvent"
      responses:
        "200":
          description: Acknowledged

  check.failed:
    post:
      tags:
        - Webhooks
      operationId: webhookCheckFailed
      summary: Check Failed
      description: |
        Fired when a verification check fails permanently after all retry attempts
        are exhausted. Includes the error category and retry count.

        > ❗ **This indicates a permanent failure**
        >
        > Zenoo has already retried the check with exponential backoff (5min → 25min →
        > 125min, capped at 240min). The `error_category` tells you why:
        >
        > | Category | Meaning |
        > |----------|---------|
        > | `Transient` | Provider was down too long — may succeed if re-initiated |
        > | `Permanent` | Invalid data or provider rejection — fix input and retry |
        > | `Rate Limited` | Provider rate limit exceeded — retry later |

        **Recommended action:** Alert your compliance team and consider manual intervention
        or re-initiating the check after addressing the underlying issue.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CheckFailedEvent"
      responses:
        "200":
          description: Acknowledged

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: |
        Project-scoped API key provided during onboarding. Each project
        has its own key scoped to the configured verification flows.

        **Key management:**
        - Store keys in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager).
        - Never commit keys to version control, embed them in client-side JavaScript, or log them in application output.
        - Staging and production use different keys. A staging key does not authenticate against the production URL.

        **Key rotation:**
        - Contact Zenoo support to rotate your API key.
        - The old key is revoked immediately — there is no grace period where both keys are active.
        - Deploy the new key to all applications before requesting rotation.

        **IP allowlisting:** Available on request for production environments. Contact your Zenoo account manager.
      x-default: "demo-api-key"

  parameters:
    ProjectHash:
      name: project_hash
      in: path
      required: true
      description: |
        Unique identifier for your Zenoo project. Determines which verification
        flow and provider configuration to use. Provided during onboarding.
      schema:
        type: string
      example: "zenoo.a1b2c3d4e5f6.prod.flow"

  responses:
    BadRequest:
      description: |
        Invalid request. Check required fields, data formats, and JSON syntax.

        > 🚧 **Do not retry 400 errors** — fix the request payload first.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            MissingField:
              summary: Required field missing from request body
              value:
                error: "VALIDATION_ERROR"
                message: "Missing required field: company_name"
                request_id: "req-a1b2c3d4"
            InvalidFormat:
              summary: Field value has wrong format
              value:
                error: "VALIDATION_ERROR"
                message: "Invalid date format for incorporation_date. Expected: YYYY-MM-DD"
                request_id: "req-b2c3d4e5"
            InvalidCountryCode:
              summary: Invalid ISO 3166-1 alpha-2 country code
              value:
                error: "VALIDATION_ERROR"
                message: "Invalid country code: UK. Use ISO 3166-1 alpha-2 (e.g., GB)"
                request_id: "req-c3d4e5f6"
            InvalidJson:
              summary: Request body is not valid JSON
              value:
                error: "INVALID_JSON"
                message: "Failed to parse request body as JSON"
                request_id: "req-d4e5f6g7"

    Unauthorized:
      description: |
        Invalid or missing API key. Check the `X-API-KEY` header.

        > 🚧 **Do not retry 401 errors** — your API key is invalid or missing.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            MissingKey:
              summary: X-API-KEY header not provided
              value:
                error: "UNAUTHORIZED"
                message: "Missing X-API-KEY header"
                request_id: "req-a1b2c3d4"
            InvalidKey:
              summary: API key is invalid or revoked
              value:
                error: "UNAUTHORIZED"
                message: "Invalid API key"
                request_id: "req-b2c3d4e5"

    RateLimited:
      description: |
        Rate limit exceeded. Respect the `Retry-After` header value.

        **Per-endpoint limits (production):**

        | Endpoint | Rate Limit |
        |----------|-----------|
        | `POST /api` (sync KYB/KYC) | 100 requests/minute |
        | `POST /init` (async) | 100 requests/minute |
        | `GET /sharable-payload/{token}` (pull) | 300 requests/minute |
        | `POST /screening/api` | 200 requests/minute |

        **Staging limits:** 50 requests/minute across all endpoints.

        > 📘 **Always use the Retry-After header** — do not guess the backoff duration.
      headers:
        Retry-After:
          description: Seconds to wait before retrying
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            RateLimited:
              summary: Too many requests — back off and retry
              value:
                error: "RATE_LIMITED"
                message: "Rate limit exceeded. Retry after 60 seconds."
                request_id: "req-a1b2c3d4"

    InternalError:
      description: |
        Internal server error. Retry with exponential backoff.

        **Client-side retry schedule:**

        | Attempt | Delay | Cumulative |
        |---------|-------|-----------|
        | 1st | Immediate | 0s |
        | 2nd | 5 seconds | 5s |
        | 3rd | 25 seconds | 30s |
        | 4th | 60 seconds | 90s |

        Maximum 4 attempts. Never retry 4xx errors — they indicate a problem with your request, not a transient server issue.

        **Server-side auto-retry (async flows):** Zenoo automatically retries transient provider failures at 5min, 25min, 125min intervals (capped at 240min). You do not need to implement retry logic for provider failures in async flows.

        > 📘 **Retry transient errors** — use exponential backoff: 0s, 5s, 25s, 60s (max 4 attempts).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            InternalError:
              summary: Unexpected server error
              value:
                error: "INTERNAL_ERROR"
                message: "An internal error occurred. Request ID: req-a1b2c3d4"
                request_id: "req-a1b2c3d4"
            ProviderError:
              summary: Upstream provider failure (registry, screening)
              value:
                error: "PROVIDER_ERROR"
                message: "Upstream provider temporarily unavailable. The request will be retried automatically."
                request_id: "req-f6g7h8i9"

  schemas:
    # ──────────────────────────────────────────────
    # Request Schemas
    # ──────────────────────────────────────────────

    KybCompanyRequest:
      type: object
      description: |
        Company data for KYB verification. At minimum, provide `company_name`,
        `registration_number`, and `country`. Additional fields improve match
        quality and enable richer compliance reports.
      required:
        - company_name
        - registration_number
        - country
      properties:
        company_name:
          type: string
          description: |
            Legal name of the company as registered. Must match the official
            registry name for successful verification. Partial names may cause
            registry lookup failures.
          minLength: 1
          maxLength: 255
          example: "Acme Holdings Ltd"
        registration_number:
          type: string
          description: |
            Official company registration number (e.g., Companies House number
            for UK). Format varies by jurisdiction.
          minLength: 1
          maxLength: 50
          example: "12345678"
        country:
          type: string
          description: |
            Country of incorporation as ISO 3166-1 alpha-2 code (uppercase, exactly 2 characters).

            **Common mistakes:**

            | Wrong | Correct | Why |
            |-------|---------|-----|
            | `UK` | `GB` | ISO standard uses `GB` for United Kingdom |
            | `USA` | `US` | Must be exactly 2 characters |
            | `BVI` | `VG` | British Virgin Islands is `VG` in ISO 3166-1 |
            | `UAE` | `AE` | United Arab Emirates is `AE` |
            | `gb` | `GB` | Must be uppercase |

            Invalid codes return `400 VALIDATION_ERROR`.
          pattern: "^[A-Z]{2}$"
          minLength: 2
          maxLength: 2
          example: "GB"
        company_type:
          type: string
          description: Legal entity type
          example: "Private Limited Company (Ltd)"
        incorporation_date:
          type: string
          format: date
          description: Date of incorporation (ISO 8601)
          example: "2015-03-15"
        industry:
          type: string
          description: Industry sector or classification
          example: "Financial Services"
        address_line_1:
          type: string
          description: Registered address street
          example: "123 High Street"
        city:
          type: string
          description: Registered address city
          example: "London"
        postcode:
          type: string
          description: Registered address postal code
          example: "EC1A 1BB"
        annual_revenue:
          type: string
          description: Annual revenue in base currency (as string for precision)
          example: "5000000"
        employee_count:
          type: string
          description: Number of employees
          example: "50"
        sic_codes:
          type: string
          description: SIC industry classification codes (comma-separated)
          example: "64110,64999"
        status:
          type: string
          description: Current company status
          enum:
            - Active
            - Dissolved
            - Struck Off
          example: "Active"
        external_reference:
          type: string
          description: |
            Your reference ID for this verification (e.g., loan reference, case ID).
            Used for correlation and idempotency — sending the same reference twice
            returns the existing case tokens without creating a duplicate.

            **Idempotency behavior:** If you submit the same `external_reference` again,
            the API returns the existing case's tokens. No duplicate verification is created
            and no additional charges apply. The second request's body fields are ignored.

            **Deterministic reference patterns:**
            - `ONBOARD-{customer_id}` — initial onboarding
            - `REVIEW-{customer_id}-{quarter}` — periodic review
            - `REVERIFY-{customer_id}-{reason}` — triggered re-verification

            > 👍 **Always include this in production** — it prevents duplicate verifications
            > and enables you to correlate results with your internal records.
          maxLength: 255
          example: "LOAN-2026-0042"
        directors:
          type: array
          description: Known directors to include in the verification
          items:
            $ref: "#/components/schemas/Director"
        ubos:
          type: array
          description: Known beneficial owners to include in the verification
          items:
            $ref: "#/components/schemas/BeneficialOwner"

    KycIndividualRequest:
      type: object
      description: |
        Individual data for KYC verification. At minimum, provide `first_name`
        and `last_name`. For best results, include `date_of_birth` and `country`
        to reduce screening false positives.
      required:
        - first_name
        - last_name
      properties:
        first_name:
          type: string
          description: Individual's first (given) name as it appears on their ID document.
          minLength: 1
          maxLength: 100
          example: "Jane"
        last_name:
          type: string
          description: Individual's last (family) name as it appears on their ID document.
          minLength: 1
          maxLength: 100
          example: "Smith"
        date_of_birth:
          type: string
          format: date
          description: Date of birth (ISO 8601)
          example: "1990-06-15"
        email:
          type: string
          format: email
          description: Email address
          example: "jane.smith@example.com"
        phone:
          type: string
          description: |
            Phone number in E.164 format (e.g., `+447700900123`). Required for
            phone verification checks. Include the country code prefix.
          pattern: "^\\+[1-9]\\d{1,14}$"
          example: "+447700900123"
        address:
          type: string
          description: Street address
          example: "42 Baker Street"
        city:
          type: string
          description: City
          example: "London"
        postal_code:
          type: string
          description: Postal/ZIP code
          example: "NW1 6XE"
        country:
          type: string
          description: |
            Country as ISO 3166-1 alpha-2 code (uppercase). Common mistakes: use `GB` not `UK`,
            `US` not `USA`, `VG` not `BVI`, `AE` not `UAE`. Must be uppercase.
          pattern: "^[A-Z]{2}$"
          example: "GB"
        nationality:
          type: string
          description: Nationality (ISO 3166-1 alpha-2)
          example: "GB"
        document_type:
          type: string
          description: Preferred document type for verification
          enum:
            - passport
            - national_id
            - driving_licence
          example: "passport"
        external_reference:
          type: string
          description: |
            Your reference ID for correlation and idempotency. Sending the same
            reference twice returns existing tokens without creating a duplicate.
            Use deterministic patterns derived from your data (e.g., `KYC-{customer_id}`).
          maxLength: 255
          example: "APP-2026-0123"

    ScreeningRequest:
      type: object
      description: |
        Entity data for AML screening. At minimum, provide `name`. For persons,
        strongly recommend including `date_of_birth` — this is the single most
        effective field for reducing false positives on common names.
      required:
        - name
      properties:
        name:
          type: string
          description: |
            Full name of the person or company to screen. For persons, use
            "FirstName LastName" format. For companies, use the legal name.
          minLength: 1
          maxLength: 255
          example: "John Smith"
        date_of_birth:
          type: string
          format: date
          description: Date of birth for person screening (improves match accuracy)
          example: "1975-03-20"
        country:
          type: string
          description: |
            Country as ISO 3166-1 alpha-2 code (uppercase) for jurisdiction filtering.
            Common mistakes: use `GB` not `UK`, `US` not `USA`, `VG` not `BVI`, `AE` not `UAE`.
          pattern: "^[A-Z]{2}$"
          example: "GB"
        entity_type:
          type: string
          description: Type of entity being screened
          enum:
            - person
            - company
          default: person
          example: "person"
        categories:
          type: array
          description: |
            Screening categories to check. If omitted, all categories are checked.
          items:
            type: string
            enum:
              - pep
              - sanctions
              - adverse_media
              - watchlist
          example:
            - "pep"
            - "sanctions"
            - "adverse_media"

    Director:
      type: object
      properties:
        first_name:
          type: string
          description: Director's first name
          example: "Jane"
        last_name:
          type: string
          description: Director's last name
          example: "Smith"
        date_of_birth:
          type: string
          format: date
          description: Date of birth (ISO 8601)
          example: "1980-05-20"
        nationality:
          type: string
          description: Nationality (ISO 3166-1 alpha-2)
          example: "GB"
        role:
          type: string
          description: Director role (e.g., Director, Secretary, Chair)
          default: "Director"
          example: "Director"
        appointed_date:
          type: string
          format: date
          description: Date of appointment
          example: "2015-03-15"
        zenoo_entity_id:
          type: string
          description: Your entity reference for correlation with results
          example: "dir-001"

    BeneficialOwner:
      type: object
      properties:
        first_name:
          type: string
          description: UBO's first name
          example: "John"
        last_name:
          type: string
          description: UBO's last name
          example: "Doe"
        date_of_birth:
          type: string
          format: date
          description: Date of birth (ISO 8601)
          example: "1975-11-10"
        nationality:
          type: string
          description: Nationality (ISO 3166-1 alpha-2)
          example: "GB"
        ownership_percentage:
          type: number
          minimum: 0
          maximum: 100
          description: Ownership stake as percentage (0-100)
          example: 75
        role:
          type: string
          description: Ownership role
          default: "UBO"
          example: "UBO"
        zenoo_entity_id:
          type: string
          description: Your entity reference for correlation with results
          example: "ubo-001"

    # ──────────────────────────────────────────────
    # Response Schemas
    # ──────────────────────────────────────────────

    TokenResponse:
      type: object
      properties:
        tokens:
          type: object
          properties:
            pull:
              type: string
              description: |
                Token for retrieving results via the pull endpoint.
                Valid for 30 days after results become available.
            start:
              type: string
              description: |
                Token for constructing the client-facing verification URL.
                Used as: `https://{base_url}/{project_hash}/?t={start_token}`

    KybCompanyResponse:
      type: object
      description: |
        Full KYB verification results returned synchronously or via pull token.
        Contains company verification data, screening results, director/UBO details,
        and check summaries.
      properties:
        company:
          type: object
          description: Verified company information from registry
          properties:
            legal_name:
              type: string
              description: Registry-verified legal name
            registration_number:
              type: string
              description: Registry-verified registration number
            jurisdiction:
              type: string
              description: Country of incorporation (ISO 3166-1 alpha-2)
            company_status:
              type: string
              description: Current registry status
              enum:
                - Active
                - Dissolved
                - Struck Off
            company_type:
              type: string
              description: Legal entity type (Ltd, PLC, LLP, etc.)
            incorporation_date:
              type: string
              format: date
            registered_address:
              type: string
            sic_codes:
              type: string
            verification_status:
              type: string
              enum:
                - Verified
                - Failed
                - Pending
            verified_at:
              type: string
              format: date-time
        screening:
          $ref: "#/components/schemas/ScreeningSummary"
        directors:
          type: array
          items:
            $ref: "#/components/schemas/DirectorResult"
        ubos:
          type: array
          items:
            $ref: "#/components/schemas/UboResult"
        checks:
          type: object
          description: Summary of individual checks performed
          properties:
            registration:
              $ref: "#/components/schemas/CheckSummaryItem"
            directors:
              $ref: "#/components/schemas/CheckSummaryItem"
            structure:
              $ref: "#/components/schemas/CheckSummaryItem"
            screening:
              $ref: "#/components/schemas/CheckSummaryItem"

    KycIndividualResponse:
      type: object
      description: KYC verification results for an individual
      properties:
        overall_verdict:
          $ref: "#/components/schemas/Verdict"
        risk_tier:
          $ref: "#/components/schemas/RiskTier"
        case_reference:
          type: string
          description: Zenoo case reference
        external_reference:
          type: string
          description: Your reference ID if provided in the request
        identity:
          type: object
          description: Identity verification results
          properties:
            verified:
              type: boolean
            decision:
              type: string
              enum: [Pass, Refer, Fail]
            score:
              type: number
              description: Verification confidence score (0-100)
            authentication_id:
              type: string
        document:
          type: object
          description: Document verification results
          properties:
            verified:
              type: boolean
            document_type:
              type: string
              description: "Detected document type (Passport, ID Card, etc.)"
            document_country:
              type: string
              description: Issuing country
            validation_result:
              type: string
              enum: [PASSED, ATTENTION, FAILED]
            tamper_result:
              type: string
              enum: [Pass, Warning, Failed]
            extracted_data:
              type: object
              description: OCR-extracted fields from the document
              properties:
                full_name:
                  type: string
                date_of_birth:
                  type: string
                  format: date
                expiration_date:
                  type: string
                  format: date
                gender:
                  type: string
        biometric:
          type: object
          description: Biometric liveness check results
          properties:
            liveness_verified:
              type: boolean
            liveness_assessment:
              type: string
              enum: [Live, Spoof]
            liveness_score:
              type: number
              description: Liveness confidence score (0-100)
            face_match:
              type: boolean
              description: Whether face matches document photo
            face_match_score:
              type: number
        screening:
          $ref: "#/components/schemas/ScreeningSummary"
        phone_verification:
          type: object
          description: Phone number verification results
          properties:
            verified:
              type: boolean
            carrier:
              type: string
            phone_type:
              type: string
              enum: [mobile, landline, voip]
        email_verification:
          type: object
          description: Email address verification results
          properties:
            verified:
              type: boolean
            risk_score:
              type: number
              description: Email risk score (0-100). Lower is better.
              minimum: 0
              maximum: 100
            deliverable:
              type: boolean
        address_verification:
          type: object
          description: Address verification results
          properties:
            verified:
              type: boolean
            match_level:
              type: string
              enum: [full, partial, none]
        compliance_metadata:
          type: object
          description: Compliance workflow metadata
          properties:
            cdd_completed:
              type: boolean
              description: Whether Customer Due Diligence was performed
            edd_required:
              type: boolean
              description: Whether Enhanced Due Diligence was triggered
            reviewed_by:
              type: string
              description: "'system' for automated reviews, analyst name for manual"

    ScreeningResponse:
      type: object
      description: AML screening results with match details
      properties:
        pep_status:
          type: string
          description: PEP screening result
          enum:
            - No Hit
            - Hit
          x-enumDescriptions:
            No Hit: No PEP matches found
            Hit: One or more PEP matches found — review required
        sanctions_status:
          type: string
          description: Sanctions screening result
          enum:
            - No Hit
            - Hit
          x-enumDescriptions:
            No Hit: No sanctions matches found
            Hit: One or more sanctions matches found — immediate action required
        adverse_media_status:
          type: string
          description: Adverse media screening result
          enum:
            - No Hit
            - Hit
          x-enumDescriptions:
            No Hit: No adverse media found
            Hit: Adverse media articles found — review recommended
        screening_provider:
          type: string
          description: Screening database provider
          example: "WorldCheck"
        screening_completed_at:
          type: string
          format: date-time
          description: When the screening was performed
        matches:
          type: array
          description: Detailed match results (empty array if no hits)
          items:
            $ref: "#/components/schemas/ScreeningMatch"

    ScreeningMatch:
      type: object
      description: Individual screening match from WorldCheck or similar provider
      properties:
        matchScore:
          type: number
          minimum: 0
          maximum: 100
          description: Match confidence score (0-100). Scores above 85 are strong matches.
          example: 92.5
        primaryName:
          type: string
          description: Primary name of the matched entity
          example: "John Smith"
        category:
          type: string
          description: Primary match category
          example: "pep"
        categories:
          type: array
          description: All categories the entity appears in
          items:
            type: string
          example:
            - "pep-class-1"
            - "adverse-media-financial-crime"
        types:
          type: array
          description: Specific match types
          items:
            type: string
          example:
            - "pep-class-1"
            - "sanction-sdn"
        nationality:
          type: string
          example: "British"
        dateOfBirth:
          type: string
          example: "1975-03-20"
        sources:
          type: array
          description: Data sources where the match was found
          items:
            type: string
          example:
            - "OFAC SDN"
            - "UK HMT"
        media:
          type: array
          description: Associated media articles (for adverse media matches)
          items:
            type: object
            properties:
              title:
                type: string
              url:
                type: string
                format: uri
              snippet:
                type: string
              date:
                type: string
        associates:
          type: array
          description: Known associates of the matched entity
          items:
            type: object
            properties:
              name:
                type: string
              association:
                type: string
              type:
                type: string
              comment:
                type: string
        positions:
          type: array
          description: Political positions held (for PEP matches)
          items:
            type: object
            properties:
              title:
                type: string
              country:
                type: string
              from:
                type: string
              to:
                type: string

    ComplianceReport:
      type: object
      description: |
        Full compliance report returned via the pull endpoint. Structure varies
        based on the verification type (KYB, KYC, or screening).

        **Automatic enrichments included:**

        | Enrichment | Trigger | What It Adds |
        |-----------|---------|-------------|
        | UBO Discovery | KYB registry check | Beneficial owners extracted from registry, auto-screened |
        | Company Intelligence | KYB flow | AI-powered risk insights, public filings, industry analysis |
        | Document Forensics | KYC document upload | Tamper detection, authenticity validation, image quality |
        | Risk Model Evaluation | All flows | 4-dimension risk scoring (entity, country, screening, product) |
        | Alert Generation | Screening matches | Compliance alerts for PEP, sanctions, adverse media hits |
        | Cross-Case Deduplication | Returning entities | Reuses valid checks from previous verifications |

        **Check validity periods (for cross-case deduplication):**

        | Check Category | Validity | Notes |
        |---------------|----------|-------|
        | Identity | 365 days | Documents and database records are relatively stable |
        | Screening | 90 days | PEP/sanctions databases update frequently |
        | Company | 180 days | Registry data changes with filings |
        | Financial | 365 days | Records change annually with audits |
        | Biometric | Always re-run | Liveness must be current |
      properties:
        case_reference:
          type: string
          description: Zenoo case reference number
          example: "AML-2026-0042"
        external_reference:
          type: string
          description: Your external reference (if provided in the request)
          example: "LOAN-2026-0042"
        processing_status:
          type: string
          description: Overall processing status
          enum:
            - complete
            - processing
            - failed
        overall_verdict:
          $ref: "#/components/schemas/Verdict"
        risk_tier:
          $ref: "#/components/schemas/RiskTier"
        completed_at:
          type: string
          format: date-time
          description: When processing completed
        next_review_date:
          type: string
          format: date
          description: Recommended next review date
        company:
          type: object
          description: Company verification data (KYB flows only)
          properties:
            legal_name:
              type: string
            registration_number:
              type: string
            jurisdiction:
              type: string
            company_status:
              type: string
            company_type:
              type: string
            incorporation_date:
              type: string
              format: date
            registered_address:
              type: string
            verification_status:
              type: string
            verified_at:
              type: string
              format: date-time
        screening:
          $ref: "#/components/schemas/ScreeningSummary"
        beneficial_owners:
          type: array
          items:
            $ref: "#/components/schemas/UboResult"
        directors:
          type: array
          items:
            $ref: "#/components/schemas/DirectorResult"
        risk_assessment:
          type: object
          description: |
            Multi-dimensional risk scoring evaluated across four independent dimensions:

            1. **Entity-Attribute Risk** — Entity type, industry classification, incorporation age, revenue profile.
            2. **Country Risk** — FATF grey/black list status, Transparency International CPI scores, sanctions exposure.
            3. **Screening Risk** — PEP, sanctions, adverse media, and watchlist matches across all screened entities.
            4. **Product/Channel Risk** — Risk profile of the product type and onboarding channel.

            **Score-to-tier mapping:**

            | Score | Tier | Meaning |
            |-------|------|---------|
            | 0–30 | Low | Standard monitoring sufficient |
            | 31–60 | Medium | Review recommended, enhanced monitoring |
            | 61–100 | High | Enhanced due diligence required |

            A single high-severity factor (e.g., sanctions hit) can push the score into the High tier even if other dimensions are clean.
          properties:
            overall_score:
              type: number
              minimum: 0
              maximum: 100
              description: Numeric risk score (0-100). Higher means riskier.
            tier:
              $ref: "#/components/schemas/RiskTier"
            factors:
              type: array
              items:
                type: object
                properties:
                  factor:
                    type: string
                    example: "Jurisdiction"
                  severity:
                    type: string
                    enum: [Low, Medium, High]
                  detail:
                    type: string
        checks_summary:
          type: object
          description: Summary of all verification checks
          properties:
            total:
              type: integer
            passed:
              type: integer
            referred:
              type: integer
            failed:
              type: integer
            checks:
              type: array
              items:
                type: object
                properties:
                  type:
                    type: string
                    description: Check type name
                  status:
                    type: string
                    enum: [Pass, Refer, Fail]
                  provider:
                    type: string
                  matches:
                    type: integer
                    description: Number of screening matches (if applicable)
                  completed_at:
                    type: string
                    format: date-time
        compliance_metadata:
          type: object
          properties:
            cdd_completed:
              type: boolean
              description: Whether Customer Due Diligence was performed
            cdd_completed_at:
              type: string
              format: date-time
            edd_required:
              type: boolean
              description: Whether Enhanced Due Diligence was triggered
            reviewed_by:
              type: string
              description: "'system' for automated reviews, analyst name for manual"
            case_url:
              type: string
              format: uri
              description: Direct link to the case in Zenoo

    # ──────────────────────────────────────────────
    # Shared Sub-Schemas
    # ──────────────────────────────────────────────

    ScreeningSummary:
      type: object
      description: Summary of screening results across all categories
      properties:
        pep_status:
          type: string
          enum: [No Hit, Hit]
        sanctions_status:
          type: string
          enum: [No Hit, Hit]
        adverse_media_status:
          type: string
          enum: [No Hit, Hit]
        adverse_media_count:
          type: integer
          description: Number of adverse media matches
        screening_provider:
          type: string
          example: "WorldCheck"
        screening_completed_at:
          type: string
          format: date-time

    DirectorResult:
      type: object
      properties:
        name:
          type: string
          example: "Jane Smith"
        date_of_birth:
          type: string
          format: date
        role:
          type: string
          example: "Director"
        pep_status:
          type: string
          enum: [No Hit, Hit]
        sanctions_status:
          type: string
          enum: [No Hit, Hit]
        screening_completed_at:
          type: string
          format: date-time

    UboResult:
      type: object
      properties:
        name:
          type: string
          example: "John Doe"
        date_of_birth:
          type: string
          format: date
        nationality:
          type: string
          example: "GB"
        ownership_percentage:
          type: number
          example: 75
        pep_status:
          type: string
          enum: [No Hit, Hit]
        sanctions_status:
          type: string
          enum: [No Hit, Hit]
        identity_verified:
          type: boolean
        verification_date:
          type: string
          format: date-time

    CheckSummaryItem:
      type: object
      properties:
        provider:
          type: string
          example: "Registry"
        external_ref:
          type: string
        result:
          type: string
          enum: [Pass, Refer, Fail]

    # ──────────────────────────────────────────────
    # Webhook Event Schemas
    # ──────────────────────────────────────────────

    VerificationCompletedEvent:
      type: object
      properties:
        event_type:
          type: string
          enum: [verification.completed]
        timestamp:
          type: string
          format: date-time
        journey_id:
          type: string
          description: Unique journey identifier
        callback_reference:
          type: string
          description: Your external reference for correlation
        status:
          type: string
          enum: [completed]
        data:
          type: object
          description: |
            Verification results. Structure depends on the flow type
            (KYB, KYC, or screening). See ComplianceReport schema.

    ScreeningCompletedEvent:
      type: object
      properties:
        event_type:
          type: string
          enum: [screening.completed]
        timestamp:
          type: string
          format: date-time
        entity_name:
          type: string
        entity_type:
          type: string
          enum: [person, company]
        results:
          $ref: "#/components/schemas/ScreeningSummary"

    JourneyAbandonedEvent:
      type: object
      properties:
        event_type:
          type: string
          enum: [journey.abandoned]
        timestamp:
          type: string
          format: date-time
        journey_id:
          type: string
        callback_reference:
          type: string

    JourneyExpiredEvent:
      type: object
      properties:
        event_type:
          type: string
          enum: [journey.expired]
        timestamp:
          type: string
          format: date-time
        journey_id:
          type: string
        callback_reference:
          type: string

    CheckFailedEvent:
      type: object
      properties:
        event_type:
          type: string
          enum: [check.failed]
        timestamp:
          type: string
          format: date-time
        check_type:
          $ref: "#/components/schemas/CheckTypeCode"
        error_category:
          $ref: "#/components/schemas/ErrorCategory"
        retry_count:
          type: integer
          description: Number of retry attempts made before failure

    # ──────────────────────────────────────────────
    # Enum & Utility Schemas
    # ──────────────────────────────────────────────

    Verdict:
      type: string
      description: Overall verification verdict
      enum:
        - Pass
        - Refer
        - Fail
      x-enumDescriptions:
        Pass: All checks passed — entity is verified with no issues found
        Refer: One or more checks require manual review (e.g., screening matches, document issues)
        Fail: Critical check failure — entity cannot be verified (e.g., sanctions hit, fraudulent document)

    RiskTier:
      type: string
      description: Risk classification tier
      enum:
        - High
        - Medium
        - Low
      x-enumDescriptions:
        High: Elevated risk — enhanced due diligence required, senior compliance review
        Medium: Moderate risk — standard review with enhanced monitoring recommended
        Low: Low risk — standard processing, automated approval eligible

    VerificationStatus:
      type: string
      description: Entity-level verification status
      enum:
        - Not Started
        - In Progress
        - Verified
        - Failed
        - Expired
      x-enumDescriptions:
        Not Started: No verification checks have been initiated
        In Progress: One or more checks are currently running
        Verified: All required checks passed successfully
        Failed: One or more critical checks failed
        Expired: Previous verification has expired and needs renewal

    CheckStatus:
      type: string
      description: Individual check execution status
      enum:
        - Pending
        - Queued
        - In Progress
        - Awaiting Client
        - Complete
        - Failed
        - Waived
        - Cancelled
        - Reused
        - Retry Scheduled
      x-enumDescriptions:
        Pending: Check created but not yet queued for execution
        Queued: Check is queued for execution
        In Progress: Check is currently being executed by the provider
        Awaiting Client: Waiting for user action (e.g., document upload, liveness check)
        Complete: Check completed successfully with results
        Failed: Check failed after all retry attempts
        Waived: Check was manually waived by a compliance analyst
        Cancelled: Check was cancelled
        Reused: Results reused from a previous valid check on the same entity
        Retry Scheduled: Check failed transiently and is scheduled for automatic retry

    CheckCategory:
      type: string
      description: Verification check category
      enum:
        - Identity
        - Screening
        - Company
        - Financial
        - EDD
      x-enumDescriptions:
        Identity: Document, biometric, database, and address verification
        Screening: PEP, sanctions, adverse media, and watchlist checks
        Company: Registry verification, director/UBO extraction
        Financial: Bank account, credit, source of funds/wealth checks
        EDD: Enhanced due diligence checks (country risk, risk profile)

    AlertCategory:
      type: string
      description: Alert categories generated from verification results
      enum:
        - PEP Match
        - Sanctions Hit
        - Adverse Media
        - Watchlist Match
        - Name Mismatch
        - DOB Mismatch
        - API Failure
        - Criminal Investigation
        - Regulatory Action
      x-enumDescriptions:
        PEP Match: Entity matched a Politically Exposed Person record
        Sanctions Hit: Entity matched a sanctions list (OFAC, EU, UN, HMT, etc.)
        Adverse Media: Negative news articles found about the entity
        Watchlist Match: Entity matched a law enforcement or watchlist database
        Name Mismatch: Submitted name does not match verified name
        DOB Mismatch: Submitted date of birth does not match verified date
        API Failure: External provider API call failed after retries
        Criminal Investigation: Match related to criminal investigation records
        Regulatory Action: Match related to regulatory enforcement actions

    EntityType:
      type: string
      description: Type of entity being verified
      enum:
        - person
        - company
      x-enumDescriptions:
        person: An individual (natural person)
        company: A legal entity (corporation, partnership, trust, etc.)

    CheckTypeCode:
      type: string
      description: |
        Verification check type codes used in the `checks_summary` and `check.failed` webhook events.

        **Check categories and codes:**

        | Category | Code | Name | Method |
        |----------|------|------|--------|
        | Identity | `IDV_DOCUMENT` | Identity Document | API |
        | Identity | `IDV_BIOMETRIC` | Biometric Verification | API |
        | Identity | `IDV_DATABASE` | Database Verification | API |
        | Identity | `IDV_ADDRESS` | Address Verification | API |
        | Identity | `IDV_PHONE` | Phone Verification | API |
        | Identity | `IDV_EMAIL` | Email Verification | API |
        | Screening | `SCR_PEP` | PEP Screening | API |
        | Screening | `SCR_SANCTIONS` | Sanctions Screening | API |
        | Screening | `SCR_ADVERSE_MEDIA` | Adverse Media | API |
        | Screening | `SCR_WATCHLIST` | Watchlist Screening | API |
        | Company | `KYB_REGISTRATION` | Company Registration | API |
        | Company | `KYB_DIRECTORS` | Directors Verification | API |
        | Company | `KYB_UBO` | UBO Verification | API |
        | Financial | `FIN_CREDIT` | Credit Check | API |
        | Financial | `FIN_BANK_ACCOUNT` | Bank Account Verification | Document |
        | Financial | `FIN_SOF` | Source of Funds | Document |
        | Financial | `FIN_SOW` | Source of Wealth | Document |
      enum:
        - IDV_DOCUMENT
        - IDV_BIOMETRIC
        - IDV_DATABASE
        - IDV_ADDRESS
        - IDV_PHONE
        - IDV_EMAIL
        - SCR_PEP
        - SCR_SANCTIONS
        - SCR_ADVERSE_MEDIA
        - SCR_WATCHLIST
        - KYB_REGISTRATION
        - KYB_DIRECTORS
        - KYB_UBO
        - FIN_CREDIT
        - FIN_BANK_ACCOUNT
        - FIN_SOF
        - FIN_SOW
      x-enumDescriptions:
        IDV_DOCUMENT: "Government ID scanning with OCR extraction (passport, national ID, driving licence)"
        IDV_BIOMETRIC: "Selfie capture with liveness detection and face matching against document photo"
        IDV_DATABASE: "Name, DOB, and address validation against official databases"
        IDV_ADDRESS: "Address validation and geocoding against postal reference databases"
        IDV_PHONE: "Phone number verification including carrier lookup and line type detection"
        IDV_EMAIL: "Email deliverability check, disposable email detection, and fraud risk scoring"
        SCR_PEP: "Screens against Politically Exposed Person databases (class 1-4)"
        SCR_SANCTIONS: "Screens against OFAC SDN, EU, UN, and UK HMT consolidated sanctions lists"
        SCR_ADVERSE_MEDIA: "Screens against negative news databases covering financial crime, fraud, terrorism"
        SCR_WATCHLIST: "Screens against law enforcement databases (Interpol, FBI, national fugitive lists)"
        KYB_REGISTRATION: "Verifies company against the official registry (legal name, status, address, SIC codes)"
        KYB_DIRECTORS: "Extracts and verifies directors from the company registry with auto-screening"
        KYB_UBO: "Extracts beneficial owners from the company registry with auto-screening"
        FIN_CREDIT: "Credit history verification against credit bureau records"
        FIN_BANK_ACCOUNT: "Bank statement upload and verification (account ownership, transaction activity)"
        FIN_SOF: "Source of funds documentation review (required for high-value transactions)"
        FIN_SOW: "Source of wealth documentation review (required for Enhanced Due Diligence)"

    ErrorCategory:
      type: string
      description: Classification of API errors for retry logic
      enum:
        - Transient
        - Permanent
        - Rate Limited
      x-enumDescriptions:
        Transient: Temporary failure (timeout, 5xx) — will be retried automatically
        Permanent: Permanent failure (4xx, invalid data) — will not be retried
        Rate Limited: Rate limit exceeded (429) — retried with extended backoff

    ErrorCode:
      type: string
      description: |
        Machine-readable error code returned in every error response. Use this field for
        programmatic error handling — do not parse the `message` field.

        **Retry classification:**

        | Category | Error Codes | Retryable |
        |----------|------------|-----------|
        | Client errors | VALIDATION_ERROR, INVALID_JSON, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, TOKEN_EXPIRED | No — fix the request first |
        | Transient errors | INTERNAL_ERROR, PROVIDER_ERROR, SERVICE_UNAVAILABLE | Yes — retry with exponential backoff |
        | Rate limiting | RATE_LIMITED | Yes — wait for `Retry-After` header value |
      enum:
        - VALIDATION_ERROR
        - INVALID_JSON
        - UNAUTHORIZED
        - FORBIDDEN
        - NOT_FOUND
        - TOKEN_EXPIRED
        - RATE_LIMITED
        - INTERNAL_ERROR
        - PROVIDER_ERROR
        - SERVICE_UNAVAILABLE
      x-enumDescriptions:
        VALIDATION_ERROR: "400 — Missing or invalid required fields. Fix the request payload and resubmit."
        INVALID_JSON: "400 — Request body is not valid JSON. Check for syntax errors."
        UNAUTHORIZED: "401 — Missing or invalid API key. Verify the X-API-KEY header."
        FORBIDDEN: "403 — API key does not have access to this project. Contact Zenoo support."
        NOT_FOUND: "404 — Invalid project hash, endpoint, or resource. Verify the URL."
        TOKEN_EXPIRED: "404 — Pull token has expired (30-day lifetime). Initiate a new verification."
        RATE_LIMITED: "429 — Too many requests. Wait for the Retry-After header value before retrying."
        INTERNAL_ERROR: "500 — Unexpected server error. Retry with exponential backoff (0s, 5s, 25s, 60s)."
        PROVIDER_ERROR: "502 — Upstream provider failure. Retry with exponential backoff. Async flows retry automatically."
        SERVICE_UNAVAILABLE: "503 — Service temporarily unavailable. Retry after 30 seconds."

    Error:
      type: object
      description: Standard error response
      required:
        - error
        - message
        - request_id
      properties:
        error:
          $ref: "#/components/schemas/ErrorCode"
        message:
          type: string
          description: Human-readable error description
          example: "Missing required field: company_name"
        request_id:
          type: string
          description: Unique request identifier for support reference
          example: "req-a1b2c3d4"
