> ## 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.

# Mint a presigned S3 URL the client uploads bytes to

> Stage 1 of the upload flow. Returns a single-use presigned PUT URL (5-minute TTL, locked to the specified `contentType`) the SPA / SDK uploads raw bytes to. Bypassing the Lambda avoids the 6 MB sync-invocation cap; the Lambda only sees the resulting `image_key` reference on stage 3 (`POST /captures`).

**Auth:** the verification id in the path `:token` IS the credential (same trust model as Stripe payment-intent `client_secret`). Any caller with the token can mint an upload URL — but the URL is single-use, time-limited, and locked to a specific content-type, so a stolen URL cannot be repurposed.

**Capture id assignment:** server-side, returned to the client. The same id flows back to the server on `POST /captures` via the `image_key` field (the key contains the id), keeping the dashboard's display logic deterministic.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/verifications/{token}/captures/upload-url
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/{token}/captures/upload-url:
    post:
      tags:
        - Captures
      summary: Mint a presigned S3 URL the client uploads bytes to
      description: >-
        Stage 1 of the upload flow. Returns a single-use presigned PUT URL
        (5-minute TTL, locked to the specified `contentType`) the SPA / SDK
        uploads raw bytes to. Bypassing the Lambda avoids the 6 MB
        sync-invocation cap; the Lambda only sees the resulting `image_key`
        reference on stage 3 (`POST /captures`).


        **Auth:** the verification id in the path `:token` IS the credential
        (same trust model as Stripe payment-intent `client_secret`). Any caller
        with the token can mint an upload URL — but the URL is single-use,
        time-limited, and locked to a specific content-type, so a stolen URL
        cannot be repurposed.


        **Capture id assignment:** server-side, returned to the client. The same
        id flows back to the server on `POST /captures` via the `image_key`
        field (the key contains the id), keeping the dashboard's display logic
        deterministic.
      parameters:
        - schema:
            type: string
            pattern: ^vfy_[0-9A-HJKMNP-TV-Z]{26}$
            example: vfy_01HXYZABCDEFGHJKMNPQRSTVWX
          required: true
          name: token
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestUploadUrlRequest'
            example:
              content_type: image/jpeg
              kind: image
      responses:
        '200':
          description: Presigned URL minted. Client PUTs bytes here next.
          content:
            application/json:
              schema:
                type: object
                properties:
                  upload_url:
                    type: string
                    format: uri
                  image_key:
                    type: string
                  capture_id:
                    type: string
                  expires_at:
                    type: string
                    format: date-time
                required:
                  - upload_url
                  - image_key
                  - capture_id
                  - expires_at
              example:
                upload_url: >-
                  https://rial-platform-evidence-staging-977916686093.s3.amazonaws.com/vfy_01HXYZ/cap_01ABC?X-Amz-Signature=...
                image_key: >-
                  s3://rial-platform-evidence-staging-977916686093/vfy_01HXYZ/cap_01ABC
                capture_id: cap_01ABCDEFGHJKMNPQRSTVWXYZ12
                expires_at: '2026-05-28T13:05:00.000Z'
        '400':
          description: Invalid `content_type` or missing field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_request
                  message: Request body failed validation
        '404':
          description: Unknown verification id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: not_found
                  message: Resource not found
      security:
        - captureToken: []
components:
  schemas:
    RequestUploadUrlRequest:
      type: object
      properties:
        content_type:
          type: string
        kind:
          type: string
          enum:
            - image
            - forensic_buffer
          default: image
      required:
        - content_type
      additionalProperties: false
      description: >-
        Body for `POST /v1/verifications/:token/captures/upload-url`. The
        returned presigned PUT URL is single-use, 5-minute TTL, locked to the
        specified `contentType` — a stolen URL cannot be repurposed for a
        different mime.
    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:
    captureToken:
      type: apiKey
      in: query
      name: token
      description: >-
        Possession of the verification id in the URL path
        (`/v1/verifications/:token/...`) authenticates the SPA capture flow. No
        header — the id is the credential. ULIDs are large enough that
        existence-by-id is not a meaningful leak. Used for `POST
        /captures/upload-url`, `POST /captures`, `GET /v1/verify/:token`.

````