openapi: 3.1.0
info:
  title: Sentinel API
  version: "1.0.0"
  summary: KYC, KYB, AML screening, document (IDP) checks and transaction monitoring for Kenya and East Africa.
  description: |
    The Sentinel API by Deeptrack lets your systems run identity, business and
    AML checks without opening the dashboard.

    **Authentication.** Send your API key as a bearer token:
    `Authorization: Bearer gt_live_…` (or `gt_test_…`). Create keys in
    Sentinel under **Developer → API Keys**. Each key has scopes; a key
    without the scope a route needs gets `403`.

    **Test mode.** Test keys (`gt_test_`) return a sandbox result at once
    (`status: completed`, `verdict: pass`, `sandbox: true`), are never
    charged, and store none of the names, numbers or images you send.

    **Asynchronous checks.** `POST /v1/verify/*` answers `202` with an `id`.
    The result arrives by webhook (`sentinel.scan.complete`) or with
    `GET /v1/verify/{id}`.

    **Errors.** Every error is JSON with an `error` message; validation
    errors add `details`, describing what each field takes.

    **Rate limits.** Per key, by plan (Trial 10/min, Starter 60, Growth 200,
    Business 500, Enterprise 1,000), plus an organization-wide cap. A limited
    request gets `429` with `Retry-After` in seconds.

    **Pagination.** List endpoints take `limit` (1–100) and `before`; pass
    the previous answer's `nextCursor` as `before` to get the next page.
    `nextCursor` is `null` on the last page.

    Guides with curl, Node and Python examples are in the Sentinel manual,
    chapters C1 to C9.
  contact:
    name: Deeptrack
    url: https://deeptrack.io
    email: tech@deeptrack.io
  x-changelog: See the manual, chapter C9 (API reference, OpenAPI and Postman).
servers:
  - url: https://robust-parakeet-542.convex.site
    description: Sentinel production API. Shown on the API Keys page; it may move to an api.* address, which will be announced in C9.
security:
  - bearerAuth: []
tags:
  - name: KYC
    description: Identity checks (document, liveness, face match, IPRS and AML).
  - name: KYB
    description: Business checks (registration, address, directors and owners).
  - name: Documents
    description: IDP document checks (identity documents, proof of address, KRA PIN certificates).
  - name: AML
    description: Sanctions, PEP and watch-list screening, and ongoing monitoring.
  - name: Transactions
    description: Transaction monitoring (Business and Enterprise plans, and the trial).
  - name: Capture links
    description: Links a customer opens on their phone to capture their ID and a selfie.
  - name: Results
    description: Read checks and their results.
  - name: Credits
    description: Credit balance, prices and usage.

paths:
  /v1/verify/idp:
    post:
      tags: [KYC]
      operationId: createKycCheck
      summary: Run a KYC identity check
      description: |
        Document scan, liveness, face match, IPRS (Kenya's national register)
        and AML screening of the person. Scope `verify:kyc`. Each media field
        is at most 1,000,000 base64 characters.
      x-scope: verify:kyc
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KycRequest" }
            example:
              documentFrontBase64: "/9j/4AAQSkZJRg…"
              documentBackBase64: "/9j/4AAQSkZJRg…"
              livenessFramesBase64: "/9j/4AAQSkZJRg…"
              livenessMediaType: jpeg_frames
              idNumber: "30458812"
              firstName: Wanjiku
              lastName: Mwangi
              dateOfBirth: "1992-04-17"
              gender: F
      responses:
        "202": { $ref: "#/components/responses/CheckAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/verify/aml:
    post:
      tags: [AML]
      operationId: createAmlScreening
      summary: Screen a person or business
      description: |
        Screens a name against sanctions, PEP (politically exposed persons),
        Kenyan risk lists and watch lists. With `monitor: true` (live keys)
        the subject is also re-screened daily and new hits are sent as
        `sentinel.monitoring.alert`. Scope `verify:aml`.
      x-scope: verify:aml
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AmlRequest" }
            example:
              subjectName: Wanjiku Mwangi
              entityType: individual
              country: KE
              dateOfBirth: "1992-04-17"
              idNumber: "30458812"
              monitor: true
              externalRef: CUST-1001
      responses:
        "202": { $ref: "#/components/responses/CheckAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/verify/document:
    post:
      tags: [Documents]
      operationId: createDocumentCheck
      summary: Check a document
      description: |
        Verifies an identity document, a proof of address or a KRA PIN
        certificate with vision, OCR and forensic checks, and optionally
        matches it to what the customer declared. Scope `verify:document`.
      x-scope: verify:document
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DocumentRequest" }
            example:
              documentKind: proof_of_address
              documentFrontBase64: "JVBERi0xLjcK…"
              expectedName: Wanjiku Mwangi
              expectedAddress: "Kilimani, Nairobi"
              maxDocumentAgeDays: 90
              externalRef: CUST-1001
      responses:
        "202": { $ref: "#/components/responses/CheckAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/verify/kyb:
    post:
      tags: [KYB]
      operationId: createKybCheck
      summary: Verify a business
      description: |
        Checks the registration certificate (or CR12) and proof of address,
        looks the business up in the register when available, and screens
        the business and every director and owner. Each file is at most
        4,000,000 base64 characters (about 3 MB). Scope `verify:kyb`.
      x-scope: verify:kyb
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KybRequest" }
            example:
              businessName: Mwangi Traders Limited
              registrationNumber: PVT-AB1234
              incorporationCountry: KE
              businessType: Private limited company
              externalRef: BIZ-2001
              registrationDocBase64: "JVBERi0xLjcK…"
              proofOfAddressBase64: "JVBERi0xLjcK…"
              directors:
                - firstName: Wanjiku
                  lastName: Mwangi
                  email: wanjiku@example.co.ke
                  position: Director
                  shareholding: "60%"
                  dateOfBirth: "1992-04-17"
                  idNumber: "30458812"
      responses:
        "202": { $ref: "#/components/responses/CheckAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/verify:
    get:
      tags: [Results]
      operationId: listChecks
      summary: List checks
      description: Your organization's checks, newest first. Scope `results:read`.
      x-scope: results:read
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [queued, processing, completed, failed] }
        - name: type
          in: query
          schema: { type: string, enum: [idp, kyb, aml, liveness, kyi, document] }
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
      responses:
        "200":
          description: A page of checks.
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CheckSummary" }
                  nextCursor: { $ref: "#/components/schemas/NextCursor" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/verify/{id}:
    get:
      tags: [Results]
      operationId: getCheck
      summary: Get a check and its result
      description: |
        One check. A completed AML check adds `screening`, a document check
        `document`, and a KYB check `kyb`. Scope `results:read`.
      x-scope: results:read
      parameters:
        - name: id
          in: path
          required: true
          description: The check's id, for example `ver_3f9a1c0b7d2e4a51…`.
          schema: { type: string, pattern: "^[A-Za-z0-9_-]{1,128}$" }
      responses:
        "200":
          description: The check.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Check" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/monitoring/subjects:
    get:
      tags: [AML]
      operationId: listMonitoredSubjects
      summary: List monitored subjects
      description: "Subjects in ongoing AML monitoring, newest first. Enrol one with `POST /v1/verify/aml` and `monitor: true`. Scope `monitoring`."
      x-scope: monitoring
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
      responses:
        "200":
          description: A page of monitored subjects.
          content:
            application/json:
              schema:
                type: object
                required: [subjects, nextCursor]
                properties:
                  subjects:
                    type: array
                    items: { $ref: "#/components/schemas/MonitoredSubject" }
                  nextCursor: { $ref: "#/components/schemas/NextCursor" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/monitoring/subjects/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The monitored subject's id, `mon_…`.
        schema: { type: string, pattern: "^mon_[A-Za-z0-9]{1,64}$" }
    get:
      tags: [AML]
      operationId: getMonitoredSubject
      summary: Get a monitored subject
      x-scope: monitoring
      responses:
        "200":
          description: The subject.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MonitoredSubject" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      tags: [AML]
      operationId: removeMonitoredSubject
      summary: Stop monitoring a subject
      x-scope: monitoring
      responses:
        "200":
          description: Removed.
          content:
            application/json:
              schema:
                type: object
                required: [id, status]
                properties:
                  id: { type: string, examples: [mon_7Hc2pQ9sLw4Xz1Ab] }
                  status: { type: string, const: removed }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/transactions:
    post:
      tags: [Transactions]
      operationId: ingestTransactions
      summary: Send transactions for monitoring
      description: |
        1 to 500 transactions per call, checked against your monitoring rules
        (large cash, structuring, rapid movement, high-risk countries,
        sanctioned counterparties). Idempotent per `externalId`: a repeat is
        counted in `duplicates`, not stored twice. Test keys validate without
        storing. Needs the Business or Enterprise plan (or the trial); other
        plans get `403`. Scope `monitoring`.
      x-scope: monitoring
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [transactions]
              properties:
                transactions:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items: { $ref: "#/components/schemas/Transaction" }
            example:
              transactions:
                - externalId: TX-000981
                  customerRef: CUST-1001
                  occurredAt: "2026-10-18T09:15:00+03:00"
                  amount: 950000
                  currency: KES
                  direction: in
                  channel: cash
                  narration: Branch deposit
      responses:
        "202":
          description: Accepted.
          content:
            application/json:
              schema:
                type: object
                required: [accepted, duplicates, rejected]
                properties:
                  accepted: { type: integer, description: "Transactions stored and checked." }
                  duplicates: { type: integer, description: "Transactions already received (same externalId)." }
                  rejected:
                    type: array
                    items:
                      type: object
                      required: [index, externalId, error]
                      properties:
                        index: { type: integer, description: "Position in your array." }
                        externalId: { type: [string, "null"] }
                        error: { type: string }
                  sandbox: { type: boolean, description: "Test keys only." }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/capture-links:
    post:
      tags: [Capture links]
      operationId: createCaptureLink
      summary: Create a capture link
      description: |
        A link the customer opens on their phone to photograph their ID and
        take a selfie; their submission becomes an ordinary KYC check
        (charged when they submit, result by webhook). Links last 72 hours.
        The `link` address is returned only in this answer. With
        `method: link` you deliver it yourself. SMS and WhatsApp delivery
        are not switched on yet: use `link`. Scope `capture`.
      x-scope: capture
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CaptureLinkRequest" }
            example:
              method: link
              language: sw
              prefill: { firstName: Wanjiku, lastName: Mwangi, idNumber: "30458812" }
              externalRef: CUST-1001
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/CaptureLink"
                  - type: object
                    properties:
                      link: { type: [string, "null"], format: uri, description: "The customer's address. null with a test key." }
                      sandbox: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Capture links]
      operationId: listCaptureLinks
      summary: List capture links
      x-scope: capture
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
      responses:
        "200":
          description: A page of capture links, newest first.
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CaptureLink" }
                  nextCursor: { $ref: "#/components/schemas/NextCursor" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/capture-links/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The capture link's id, `cap_…`.
        schema: { type: string, pattern: "^cap_[A-Za-z0-9]{1,64}$" }
    get:
      tags: [Capture links]
      operationId: getCaptureLink
      summary: Get a capture link
      x-scope: capture
      responses:
        "200":
          description: The link. When `status` is `submitted`, `verificationId` is the KYC check it became.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CaptureLink" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      tags: [Capture links]
      operationId: cancelCaptureLink
      summary: Cancel a capture link
      description: A pending or opened link stops working and its prefilled details are removed. A submitted or expired link is returned unchanged.
      x-scope: capture
      responses:
        "200":
          description: The link after cancelling.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CaptureLink" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/credits:
    get:
      tags: [Credits]
      operationId: getCredits
      summary: Credit balance and prices
      description: Scope `credits:read`.
      x-scope: credits:read
      responses:
        "200":
          description: Balance, plan and price list.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreditSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/credits/ledger:
    get:
      tags: [Credits]
      operationId: getCreditLedger
      summary: Credit history
      description: Every credit added or used, newest first. Scope `credits:read`.
      x-scope: credits:read
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/before"
      responses:
        "200":
          description: A page of ledger entries.
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      required: [type, amount, reason, verificationId, createdAt]
                      properties:
                        type: { type: string, enum: [allocation, deduction, refund, adjustment] }
                        amount: { type: number, description: "Credits; positive is added, negative is used." }
                        reason: { type: string }
                        verificationId: { type: [string, "null"] }
                        createdAt: { $ref: "#/components/schemas/Millis" }
                  nextCursor: { $ref: "#/components/schemas/NextCursor" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }

webhooks:
  sentinelEvent:
    post:
      summary: Event delivery to your endpoint
      description: |
        Sentinel POSTs this to each webhook endpoint you add under
        **Developer → Webhooks**. Verify `X-Sentinel-Signature-V2`: the hex
        HMAC-SHA256 of `<X-Sentinel-Timestamp>.<raw body>` keyed with the
        endpoint's signing secret (also accept `X-Sentinel-Signature-V2-Next`
        during a secret rotation), and reject timestamps more than 5 minutes
        old. Answer any 2xx within 10 seconds. Failed deliveries are retried
        after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours.
      parameters:
        - { name: X-Sentinel-Timestamp, in: header, required: true, schema: { type: string }, description: "Unix time in seconds." }
        - { name: X-Sentinel-Signature-V2, in: header, required: true, schema: { type: string } }
        - { name: X-Sentinel-Signature-V2-Next, in: header, required: false, schema: { type: string }, description: "Only during a secret rotation." }
        - { name: X-Sentinel-Signature, in: header, required: false, schema: { type: string }, description: "Older scheme (HMAC of the body alone). Prefer V2." }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
            example:
              event: sentinel.scan.complete
              scan_id: ver_3f9a1c0b7d2e4a51c8e0f6a29b4d7c13
              status: PASS
              timestamp: "2026-10-18T09:15:42.000Z"
              product: aml
              result: { verdict: pass }
      responses:
        "200":
          description: Any 2xx tells Sentinel the delivery arrived.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Your API key: gt_live_… or gt_test_…"

  parameters:
    limit:
      name: limit
      in: query
      description: Page size, 1 to 100. Default 25 for checks and credit history, 50 for capture links and monitored subjects.
      schema: { type: integer, minimum: 1, maximum: 100 }
    before:
      name: before
      in: query
      description: The `nextCursor` from the previous page.
      schema: { type: number, minimum: 0 }

  responses:
    CheckAccepted:
      description: The check was accepted. Live keys start `queued`; test keys finish at once.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/CheckAccepted" }
    BadRequest:
      description: Missing or invalid fields. `details` describes what each field takes.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "Missing required fields: subjectName", details: { subjectName: "string of at most 200 characters" } }
    Unauthorized:
      description: Missing, malformed, unknown or revoked API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Invalid API key }
    InsufficientCredits:
      description: The balance is too low for this check (live keys only).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Insufficient credits }
    Forbidden:
      description: The key lacks the scope, the IP address isn't allowed, the plan doesn't include the feature, or the account isn't active.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "This API key can't call POST /v1/verify/aml. Give it the \"verify:aml\" scope or use another key." }
    NotFound:
      description: Nothing with that id in your organization.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Verification not found }
    TooLarge:
      description: A file or media field is over its size limit.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests. Wait `Retry-After` seconds.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: Rate limit exceeded }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        details: { type: object, additionalProperties: true }

    Millis:
      type: number
      description: Unix time in milliseconds.

    NextCursor:
      type: [number, "null"]
      description: Pass as `before` to get the next page; null on the last page.

    Verdict:
      type: [string, "null"]
      enum: [pass, review, reject, null]
      description: "`pass`, `review` (a person needs to look) or `reject`; null until the check completes."

    CheckAccepted:
      type: object
      required: [id, type, status]
      properties:
        id: { type: string, examples: [ver_3f9a1c0b7d2e4a51c8e0f6a29b4d7c13] }
        type: { type: string, enum: [idp, aml, document, kyb] }
        status: { type: string, enum: [queued, completed] }
        verdict: { type: string, const: pass, description: "Test keys only." }
        sandbox: { type: boolean, description: "Test keys only." }
        monitoringId: { type: [string, "null"], description: "AML only, when `monitor: true` was sent: the monitored subject's id (null with a test key)." }

    CheckSummary:
      type: object
      required: [id, type, status, verdict, confidence, createdAt, completedAt]
      properties:
        id: { type: string }
        type: { type: string, enum: [idp, kyb, aml, liveness, kyi, document] }
        status: { type: string, enum: [queued, processing, completed, failed] }
        verdict: { $ref: "#/components/schemas/Verdict" }
        confidence: { type: [number, "null"], description: "1 for pass, 0 for reject, null for review." }
        createdAt: { $ref: "#/components/schemas/Millis" }
        completedAt: { type: [number, "null"] }

    Check:
      allOf:
        - $ref: "#/components/schemas/CheckSummary"
        - type: object
          required: [failureReason, sandbox]
          properties:
            failureReason: { type: [string, "null"] }
            sandbox: { type: boolean }
            screening: { $ref: "#/components/schemas/AmlScreening" }
            document: { $ref: "#/components/schemas/DocumentResult" }
            kyb: { $ref: "#/components/schemas/KybResult" }

    AmlScreening:
      type: object
      description: Completed AML checks only.
      properties:
        reason: { type: [string, "null"] }
        categories:
          type: [object, "null"]
          description: Hits per category (sanctions, pep, special_interest, fitness_probity, adverse_media, insolvency) and the most severe PEP class.
        sources:
          type: object
          properties:
            watchlists:
              type: array
              items:
                type: object
                properties:
                  source: { type: [string, "null"] }
                  stale: { type: boolean, description: "True when the list is older than its refresh window." }
            openSanctions: { type: [string, "null"] }
        matches:
          type: array
          items:
            type: object
            properties:
              source: { type: [string, "null"] }
              sourceRecordId: { type: [string, "null"] }
              entityName: { type: [string, "null"] }
              program: { type: [string, "null"] }
              matchScore: { type: [number, "null"], description: "0 to 100." }
              riskLevel: { type: [string, "null"] }
              matchedCountry: { type: [string, "null"] }
              topics: { type: array, items: { type: string } }
              categories: { type: array, items: { type: string } }
              pepClass: { type: [integer, "null"], description: "1 (most senior) to 4." }
              birthDateMatch: { type: [string, "null"] }

    DocumentResult:
      type: object
      description: Completed document checks only.
      properties:
        reason: { type: [string, "null"] }
        riskScore: { type: [number, "null"] }
        documentKind: { type: [string, "null"], enum: [identity, proof_of_address, kra_pin, null] }
        identity: { type: [object, "null"], description: "Fields read from an identity document." }
        proofOfAddress: { type: [object, "null"], description: "Name, address and issue date read from a proof of address." }
        kraPin: { type: [object, "null"], description: "PIN and taxpayer read from a KRA PIN certificate." }
        checks: { type: array, items: { type: object } }
        flags: { type: array, items: { type: string } }
        ocr: { type: [object, "null"] }
        forensics: { type: [object, "null"] }

    KybResult:
      type: object
      description: Completed KYB checks only.
      properties:
        reason: { type: [string, "null"] }
        certificate: { $ref: "#/components/schemas/KybPart" }
        proofOfAddress: { $ref: "#/components/schemas/KybPart" }
        registry:
          type: [object, "null"]
          properties:
            status: { type: [string, "null"] }
        parties:
          type: array
          items:
            type: object
            properties:
              name: { type: [string, "null"] }
              role: { type: [string, "null"] }
              verdict: { $ref: "#/components/schemas/Verdict" }
              reason: { type: [string, "null"] }
              hits: { type: integer }

    KybPart:
      type: [object, "null"]
      properties:
        verdict: { $ref: "#/components/schemas/Verdict" }
        reason: { type: [string, "null"] }

    KycRequest:
      type: object
      required: [documentFrontBase64, livenessFramesBase64, livenessMediaType, idNumber, firstName, lastName, dateOfBirth, gender]
      properties:
        documentFrontBase64: { type: string, description: "Base64 JPEG or PNG of the ID front." }
        documentBackBase64: { type: string, description: "Base64 of the back (ID cards and licences)." }
        livenessFramesBase64: { type: string, description: "Base64 liveness capture." }
        livenessMediaType: { type: string, enum: [jpeg_frames, mp4, webm] }
        idNumber: { type: string, maxLength: 200 }
        firstName: { type: string, maxLength: 200 }
        lastName: { type: string, maxLength: 200 }
        dateOfBirth: { type: string, maxLength: 200, description: "YYYY-MM-DD." }
        gender: { type: string, maxLength: 200 }

    AmlRequest:
      type: object
      required: [subjectName, entityType]
      properties:
        subjectName: { type: string, maxLength: 200 }
        entityType: { type: string, enum: [individual, entity] }
        country: { type: string, description: "ISO code or name." }
        aliases: { type: array, items: { type: string }, description: "Other names." }
        dateOfBirth: { type: string, description: "Individuals only: YYYY, YYYY-MM or YYYY-MM-DD." }
        nationality: { type: string, description: "Individuals only." }
        idNumber: { type: string, description: "Individuals only: national ID or passport number." }
        registrationNumber: { type: string, description: "Entities only." }
        jurisdiction: { type: string, description: "Entities only: country of incorporation." }
        monitor: { type: boolean, description: "Re-screen this subject daily and alert on new hits (live keys)." }
        externalRef: { type: string, maxLength: 100, description: "With monitor: your customer id." }

    DocumentRequest:
      type: object
      required: [documentKind, documentFrontBase64]
      properties:
        documentKind: { type: string, enum: [identity, proof_of_address, kra_pin] }
        documentFrontBase64: { type: string, description: "Base64 JPEG, PNG, WebP or PDF." }
        documentBackBase64: { type: string, description: "Identity documents only." }
        expectedName: { type: string }
        expectedAddress: { type: string, description: "Proof of address only." }
        expectedDateOfBirth: { type: string, description: "Identity documents only: YYYY-MM-DD." }
        expectedKraPin: { type: string, description: "KRA PIN certificates only." }
        maxDocumentAgeDays: { type: number, description: "Proof of address: oldest acceptable issue date, in days (default 90)." }
        externalRef: { type: string }

    KybRequest:
      type: object
      required: [businessName, registrationNumber, incorporationCountry, registrationDocBase64, proofOfAddressBase64, directors]
      properties:
        businessName: { type: string, maxLength: 200 }
        registrationNumber: { type: string, maxLength: 100, examples: [PVT-AB1234, CPR/2015/123456] }
        incorporationCountry: { type: string, maxLength: 100, examples: [KE] }
        businessType: { type: string, maxLength: 100 }
        externalRef: { type: string, maxLength: 100 }
        registrationDocBase64: { type: string, maxLength: 4000000, description: "Base64 JPEG, PNG, WebP or PDF of the registration certificate or CR12." }
        proofOfAddressBase64: { type: string, maxLength: 4000000, description: "Base64 JPEG, PNG, WebP or PDF of a proof of address in the business's name." }
        directors:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: object
            required: [firstName, lastName, email, position, dateOfBirth, idNumber]
            properties:
              firstName: { type: string, maxLength: 100 }
              lastName: { type: string, maxLength: 100 }
              email: { type: string, format: email, maxLength: 200 }
              position: { type: string, maxLength: 100 }
              shareholding: { type: string, maxLength: 20, examples: ["40%"] }
              dateOfBirth: { type: string, pattern: "^\\d{4}-\\d{2}-\\d{2}$" }
              idNumber: { type: string, maxLength: 50 }

    Transaction:
      type: object
      required: [externalId, customerRef, occurredAt, amount, direction]
      properties:
        externalId: { type: string, description: "Your unique id for the transaction." }
        customerRef: { type: string, description: "Your customer id, or the Sentinel id of their KYC or AML check." }
        occurredAt: { type: string, format: date-time }
        amount: { type: number, exclusiveMinimum: 0 }
        currency: { type: string, enum: [KES, USD], default: KES }
        direction: { type: string, enum: [in, out] }
        channel: { type: string, enum: [mpesa, cash, bank_transfer, rtgs, swift, card, cheque, other] }
        counterpartyName: { type: string, description: "Screened against sanctions and watch lists." }
        counterpartyCountry: { type: string, description: "2-letter ISO code." }
        counterpartyAccount: { type: string }
        narration: { type: string }

    CaptureLinkRequest:
      type: object
      properties:
        method: { type: string, enum: [link, sms, whatsapp], default: link }
        contact: { type: string, description: "Required for sms and whatsapp: international format, e.g. +254712345678." }
        language: { type: string, enum: [en, sw], default: en }
        prefill:
          type: object
          description: Filled in for the customer, who can't change it.
          properties:
            firstName: { type: string }
            lastName: { type: string }
            idNumber: { type: string }
            dateOfBirth: { type: string, description: "YYYY-MM-DD." }
            gender: { type: string }
          additionalProperties: false
        externalRef: { type: string, maxLength: 100 }

    CaptureLink:
      type: object
      required: [id, method, contact, language, externalRef, status, delivery, createdAt, expiresAt]
      properties:
        id: { type: string, examples: [cap_8Fq2kT0aZx91LmPw] }
        method: { type: string, enum: [link, sms, whatsapp] }
        contact: { type: [string, "null"] }
        language: { type: string, enum: [en, sw] }
        externalRef: { type: [string, "null"] }
        status: { type: string, enum: [pending, opened, submitted, expired, cancelled] }
        delivery: { type: string, enum: [not_sent, pending, sent, failed] }
        deliveryFailure: { type: [string, "null"] }
        createdAt: { $ref: "#/components/schemas/Millis" }
        expiresAt: { $ref: "#/components/schemas/Millis" }
        openedAt: { type: [number, "null"] }
        submittedAt: { type: [number, "null"] }
        verificationId: { type: [string, "null"], description: "The KYC check the submission became." }

    MonitoredSubject:
      type: object
      properties:
        id: { type: string, examples: [mon_7Hc2pQ9sLw4Xz1Ab] }
        status: { type: string, enum: [active, removed] }
        subjectName: { type: string }
        entityType: { type: string, enum: [individual, entity] }
        country: { type: [string, "null"] }
        externalRef: { type: [string, "null"] }
        hits:
          type: array
          items:
            type: object
            properties:
              key: { type: string }
              entityName: { type: string }
              categories: { type: array, items: { type: string } }
              pepClass: { type: integer }
        screenCount: { type: integer }
        alertCount: { type: integer }
        lastScreenedAt: { type: [number, "null"] }
        lastAlertAt: { type: [number, "null"] }
        nextScreenAt: { type: [number, "null"] }
        createdAt: { $ref: "#/components/schemas/Millis" }
        removedAt: { type: [number, "null"] }

    CreditSummary:
      type: object
      properties:
        balance: { type: number, description: "Credits." }
        currency: { type: string, enum: [USD, KES] }
        creditPrice: { type: number, description: "Price of one credit in your currency." }
        planBonus: { type: number, description: "Annual-prepayment bonus on your plan, as a fraction (KES plans: 0.1 = 10% more credits); 0 for USD." }
        balanceValue: { type: number, description: "What the balance is worth on your plan." }
        plan: { type: string, enum: [trial, starter, growth, business, enterprise] }
        prices:
          type: array
          items:
            type: object
            properties:
              service: { type: string }
              credits: { type: number }
              amount: { type: number }

    WebhookEvent:
      type: object
      required: [event, scan_id, status, timestamp, product]
      properties:
        event: { type: string, enum: [sentinel.scan.complete, sentinel.monitoring.alert, sentinel.review.decided, sentinel.credits.low, sentinel.test] }
        scan_id: { type: string, description: "The check's id; use it with GET /v1/verify/{id}." }
        status: { type: string, enum: [PASS, REVIEW, REJECT, FAILED] }
        timestamp: { type: string, format: date-time }
        product: { type: [string, "null"], enum: [kyc, aml, idp, kyb, liveness, null] }
        result: { type: object, description: "The check's full result; for FAILED, { failureReason }." }
        review:
          type: object
          description: sentinel.review.decided only.
          properties:
            decision: { type: string, enum: [approved, rejected] }
            decided_by: { type: string, enum: [client, deeptrack] }
            reason: { type: string }
            decided_at: { type: string, format: date-time }
        credits:
          type: object
          description: sentinel.credits.low only.
          properties:
            balance: { type: number }
            warning_level: { type: number }
        sandbox: { type: boolean }
