> ## Documentation Index
> Fetch the complete documentation index at: https://docs.get-rial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# List (and search) tenant verifications, newest first

> Newest-first list, opaque-cursor paginated. `next_cursor` from the previous page round-trips back as `?cursor=`; absent means the list is exhausted.

The list is **tenant-scoped** — operator sessions see only their own tenant's verifications. API key callers see only the tenant the key belongs to. Cross-tenant pagination is never possible.

**Search filters** (all optional, combined with AND): `status`, `verdict`, `reason_code`, `from`/`to` (inclusive bounds on `created_at`), `metadata.<key>=<value>` (exact match against the verification's `config.metadata`; repeat with different keys to AND them — e.g. `metadata.client_label=estancia-la-julia`), and `caravana` (exact match against `ocr.primary` of ANY capture on the verification — useful for looking up a verification by a plate/tag number captured in-frame). `status`/`verdict`/`reason_code`/`from`/`to` are validated: an invalid value 400s rather than silently returning an unfiltered page. Because a page can be filtered *after* the underlying DDB page is fetched (notably `caravana`), a response with fewer than `limit` items and a defined `next_cursor` does NOT mean the list is exhausted — keep paging.



## OpenAPI

````yaml /api-reference/openapi.json get /v1/verifications
openapi: 3.1.0
info:
  title: rial-platform — Verifications API
  version: 0.1.0
  description: |-
    # rial-platform — Verifications API

    HTTP control plane for the rial-platform verifications-service:
    creating verifications, capturing media against them, finalising
    cases for batch analysis, and the operator-facing dashboard.

    ## Audience

    - **Tenants** integrating rial into a claims / verification flow —
      use the operator-issued API key and call `POST /v1/verifications`,
      then send the returned `capture_url` to your end-user.
    - **The dashboard SPA** (`app.get-rial.com`) — same surface,
      authenticated via the `rial_session` cookie.
    - **The WhatsApp bot (rialclaw)** — signs requests with HMAC
      (`X-Bot-Auth`), carries the target tenant in the body.
    - **End-user browsers** capturing photos against a fresh
      verification — authenticated by possession of the path `:token`
      alone (the token IS the credential, see ADR-0002).

    ## Auth modes

    - `cookieSession` — `rial_session` cookie set by Google OAuth at
      `/v1/auth/google/callback`. Dashboard browsers.
    - `bearerApiKey` — `Authorization: Bearer pk_live_<…>` API key,
      tenant-scoped. Server-to-server integrations.
    - `captureToken` — possession of the verification id (`vfy_…`)
      in the URL path authenticates the SPA capture flow. Same trust
      model as Stripe payment intent client_secret.
    - `botHmac` — `X-Bot-Auth: <hmac>` from the WhatsApp bot.

    ## State machine

    A verification's status: `pending` → `partially_captured` →
    `completed` | `expired` | `failed`. State transitions emit
    domain events on the internal EventBridge bus; webhook events
    are translated from those and delivered to the tenant's
    configured `webhook_url` (HMAC-signed via the per-tenant
    `webhook-signing` secret). `pending`/`partially_captured` can
    also side-branch to `abandoned` — reported by the capture screen
    via `POST /v1/verifications/:token/progress` when the end user
    leaves before finishing. It is not terminal: a later capture
    resurrects the row to `partially_captured` like any other, and no
    webhook event fires for the transition.

    ## Conventions

    - Wire format is **snake_case JSON** in both directions; the
      service maps to camelCase internally.
    - Errors are `{error: {code, message, fields?}}` — `code` is
      the stable string SDKs branch on; `message` is human-readable
      English; `fields` is present only on `invalid_request`
      validation failures.
    - IDs are ULIDs with type prefixes: `vfy_`, `cap_`, `tnt_`,
      `walotp_`, `mlk_`.
    - Pagination is opaque-cursor: client passes back whatever
      `next_cursor` the previous page returned; absent means done.

    ## Out of scope here

    - Webhook payloads delivered to tenant URLs — those are
      documented separately under `docs/webhook-events.md`. This
      spec covers only the inbound HTTP surface, not the outbound
      HTTP rial → tenant calls.
    - Internal EventBridge events on the `rial-platform-events` bus
      (`verification.created`, `verification.case.completed`, …) —
      consumer-internal, not part of the public contract.
  contact:
    name: rial-platform team
    url: https://github.com/Rial-ventures-Inc/rial-platform
  license:
    name: UNLICENSED — proprietary, internal use only
servers:
  - url: https://platform-staging.get-rial.com
    description: Staging (safe to hit; ANALYSIS=stub, CASE_ANALYSIS=gemini-case)
  - url: https://platform.get-rial.com
    description: Production (live tenant traffic; do not test against)
security: []
tags:
  - name: Verifications
    description: >-
      Create, fetch, list, and finalize verifications. The core of the API —
      every tenant integration starts here.
  - name: Captures
    description: >-
      Upload media against an existing verification. Two-stage flow: mint a
      presigned S3 URL → PUT bytes direct to S3 → POST submit-capture with the
      resulting key. Hardware attestation (Cloudflare Turnstile / Apple PAT) is
      verified per-capture.
  - name: Case Analysis
    description: >-
      Batch multi-image case analysis (GET-59). Finalize a verification with a
      claimant narrative; the worker runs Gemini multi-image cross-reasoning and
      emits a structured CaseAnalysisReport. Staging-only today per ADR-002
      sibling deferral.
  - name: Public Trial
    description: >-
      Unauthenticated trial endpoint behind `get-rial.com/trial`. Each call
      mints a fresh verification under the rial. tenant tagged
      `originChannel="wa"`, so the WA-completion worker delivers the verdict to
      the configured trial number.
  - name: Templates
    description: >-
      Publish, update, revoke and inspect link templates — the reusable capture
      links behind `/l/{org}/{slug}`. Callable with a secret key; each publish
      snapshots a versioned capture spec (GET-83).
paths:
  /v1/verifications:
    get:
      tags:
        - Verifications
      summary: List (and search) tenant verifications, newest first
      description: >-
        Newest-first list, opaque-cursor paginated. `next_cursor` from the
        previous page round-trips back as `?cursor=`; absent means the list is
        exhausted.


        The list is **tenant-scoped** — operator sessions see only their own
        tenant's verifications. API key callers see only the tenant the key
        belongs to. Cross-tenant pagination is never possible.


        **Search filters** (all optional, combined with AND): `status`,
        `verdict`, `reason_code`, `from`/`to` (inclusive bounds on
        `created_at`), `metadata.<key>=<value>` (exact match against the
        verification's `config.metadata`; repeat with different keys to AND them
        — e.g. `metadata.client_label=estancia-la-julia`), and `caravana` (exact
        match against `ocr.primary` of ANY capture on the verification — useful
        for looking up a verification by a plate/tag number captured in-frame).
        `status`/`verdict`/`reason_code`/`from`/`to` are validated: an invalid
        value 400s rather than silently returning an unfiltered page. Because a
        page can be filtered *after* the underlying DDB page is fetched (notably
        `caravana`), a response with fewer than `limit` items and a defined
        `next_cursor` does NOT mean the list is exhausted — keep paging.
      parameters:
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            description: Soft cap; adapters may return fewer.
            example: 20
          required: false
          name: limit
          in: query
        - schema:
            type: string
            description: Opaque cursor from a previous response. Treat as a black box.
          required: false
          name: cursor
          in: query
        - schema:
            type: string
            enum:
              - pending
              - partially_captured
              - completed
              - expired
              - failed
              - abandoned
            description: Exact match on verification status.
          required: false
          name: status
          in: query
        - schema:
            type: string
            enum:
              - verified
              - suspicious
              - failed
            description: Exact match on `verdict.label`. Pending verifications never match.
          required: false
          name: verdict
          in: query
        - schema:
            type: string
            enum:
              - screen_detected
              - ai_generated
              - context_mismatch
              - reverse_search_match
              - depth_anomaly
            description: >-
              Exact match on `verdict.reason_code`. Verifications with no
              verdict, or a `verified` verdict, never match.
          required: false
          name: reason_code
          in: query
        - schema:
            type: string
            description: Inclusive lower bound on `created_at`. ISO 8601 date or date-time.
            example: '2026-07-01T00:00:00.000Z'
          required: false
          name: from
          in: query
        - schema:
            type: string
            description: Inclusive upper bound on `created_at`. ISO 8601 date or date-time.
            example: '2026-07-31T23:59:59.999Z'
          required: false
          name: to
          in: query
        - schema:
            type: string
            description: >-
              Exact match against `config.metadata[<key>]`. Repeatable with
              different keys (ANDed). Literal param name is `metadata.` followed
              by the metadata key, e.g.
              `metadata.client_label=estancia-la-julia`.
            example: estancia-la-julia
          required: false
          name: metadata.<key>
          in: query
        - schema:
            type: string
            description: >-
              Exact match against `ocr.primary` of any capture on the
              verification.
            example: AB123CD
          required: false
          name: caravana
          in: query
      responses:
        '200':
          description: >-
            A page of verifications matching the filters. `next_cursor` absent
            means no more pages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties: {}
                    description: Array of Verification objects.
                  next_cursor:
                    type: string
                required:
                  - items
              example:
                items:
                  - id: vfy_01HXYZABCDEFGHJKMNPQRSTVWX
                    status: completed
                    capture_url: >-
                      https://verify.get-rial.com/v/vfy_01HXYZABCDEFGHJKMNPQRSTVWX
                    created_at: '2026-05-28T13:00:00.000Z'
                    expires_at: '2026-05-28T14:00:00.000Z'
                    captures_count: 3
                    config:
                      max_captures: 3
                      steps:
                        - key: capture_1
                          type: image
                          description: ''
                          min: 3
                          max: 3
                      signals_required:
                        - screen
                        - ai
                      context:
                        kind: damage_claim
                        summary: Toyota Corolla front bumper
                      webhook_url: https://api.acmeinsurance.com/rial/webhooks
                      metadata:
                        claim_id: CLM-9912
                    narrative: >-
                      Front bumper damaged after low-speed collision in
                      supermarket parking lot.
                    verdict:
                      label: verified
                      score: 0.92
                      signals:
                        screen_detection:
                          screen_detected: false
                          confidence: 0.97
                        ai_detection:
                          label: likely_human
                          confidence: 0.88
                  - id: vfy_01HXYZABCDEFGHJKMNPQRSTVWX
                    status: pending
                    capture_url: >-
                      https://verify.get-rial.com/v/vfy_01HXYZABCDEFGHJKMNPQRSTVWX
                    created_at: '2026-05-28T13:00:00.000Z'
                    expires_at: '2026-05-28T14:00:00.000Z'
                    captures_count: 0
                    config:
                      max_captures: 3
                      steps:
                        - key: capture_1
                          type: image
                          description: ''
                          min: 3
                          max: 3
                      signals_required:
                        - screen
                        - ai
                      context:
                        kind: damage_claim
                        summary: Toyota Corolla front bumper
                      webhook_url: https://api.acmeinsurance.com/rial/webhooks
                      metadata:
                        claim_id: CLM-9912
                next_cursor: eyJwayI6ICJWRlkjdmZ5XzAxLi4uIiwic2siOiAiTUVUQSJ9
        '400':
          description: >-
            An explicit search filter (`status`, `verdict`, `reason_code`,
            `from`, `to`) was malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_request
                  message: Request body failed validation
                  fields:
                    - path: config.max_captures
                      message: must be ≤ 20
        '401':
          description: No valid session / API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: unauthorized
                  message: Missing or invalid API key
      security:
        - cookieSession: []
        - bearerApiKey: []
components:
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            fields:
              type: array
              items:
                type: object
                properties:
                  path:
                    type: string
                  message:
                    type: string
                required:
                  - path
                  - message
          required:
            - code
            - message
      required:
        - error
      description: >-
        Uniform error envelope. `code` is the stable string SDKs branch on
        (`invalid_request`, `unauthorized`, `not_found`, `expired`, `step_full`,
        `unknown_step`, `steps_incomplete`, `already_finalized`,
        `too_many_requests`, `storage_unavailable`, `internal_error`). `fields`
        is only present on `invalid_request` validation failures.
  securitySchemes:
    cookieSession:
      type: apiKey
      in: cookie
      name: rial_session
      description: >-
        Dashboard / operator session cookie. Set by `/v1/auth/google/callback`
        after Google OAuth, JWT signed with the rotating
        `rial-platform/session-signing` HMAC. Read on every request the SPA
        makes with `credentials: include`. Browser-only — server-to-server
        callers use `bearerApiKey` instead.
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: pk_live_<ulid>
      description: >-
        Tenant-scoped API key (`pk_live_<ulid>`). Minted by a rial operator via
        the admin API; stored as SHA-256 hash, plaintext returns once at mint
        time. Use `Authorization: Bearer pk_live_<…>`. The presented key is
        hashed and looked up via the `gsi-by-key-hash` GSI on
        `rial-platform-tenants` — a hash miss returns 401 without timing
        channel.

````