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

# Publish a link template

> Creates the template and its public link in one step — the link starts `active` (unless `status: "paused"` is sent) at `/l/{org}/{slug}`. The published snapshot starts at `version: 1` (GET-83); every verification minted from the link records the version it ran against.

**Side effects:** none beyond the write — publishing does NOT mint verifications or send notifications.

**Idempotency:** slugs are unique per tenant; a re-publish of a live slug returns 409 `slug_taken`.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/link-templates
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/link-templates:
    post:
      tags:
        - Templates
      summary: Publish a link template
      description: >-
        Creates the template and its public link in one step — the link starts
        `active` (unless `status: "paused"` is sent) at `/l/{org}/{slug}`. The
        published snapshot starts at `version: 1` (GET-83); every verification
        minted from the link records the version it ran against.


        **Side effects:** none beyond the write — publishing does NOT mint
        verifications or send notifications.


        **Idempotency:** slugs are unique per tenant; a re-publish of a live
        slug returns 409 `slug_taken`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                slug:
                  type: string
                  minLength: 2
                  maxLength: 40
                  pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
                name:
                  type: string
                  minLength: 1
                  maxLength: 80
                mode:
                  type: string
                  enum:
                    - capture
                    - audit
                    - hybrid
                  default: capture
                steps:
                  type: array
                  items:
                    anyOf:
                      - type: object
                        properties:
                          key:
                            type: string
                            minLength: 1
                            maxLength: 64
                            pattern: ^[a-z0-9][a-z0-9_-]*$
                          type:
                            type: string
                            enum:
                              - image
                            default: image
                          description:
                            type: string
                            maxLength: 500
                            default: ''
                          min:
                            type: integer
                            minimum: 0
                          max:
                            type: integer
                            minimum: 1
                        required:
                          - key
                          - min
                          - max
                        additionalProperties: false
                      - type: object
                        properties:
                          key:
                            type: string
                            minLength: 1
                            maxLength: 64
                            pattern: ^[a-z0-9][a-z0-9_-]*$
                          type:
                            type: string
                            enum:
                              - text
                          description:
                            type: string
                            maxLength: 500
                            default: ''
                          required:
                            type: boolean
                            default: true
                          validation:
                            type: object
                            properties:
                              min_length:
                                type: integer
                                minimum: 0
                                maximum: 2000
                              max_length:
                                type: integer
                                minimum: 1
                                maximum: 2000
                              format:
                                type: string
                                enum:
                                  - free_text
                                  - numeric_id
                                  - alphanumeric_id
                                default: free_text
                            default:
                              format: free_text
                            additionalProperties: false
                        required:
                          - key
                          - type
                        additionalProperties: false
                      - type: object
                        properties:
                          key:
                            type: string
                            minLength: 1
                            maxLength: 64
                            pattern: ^[a-z0-9][a-z0-9_-]*$
                          type:
                            type: string
                            enum:
                              - upload
                          description:
                            type: string
                            maxLength: 500
                            default: ''
                          min:
                            type: integer
                            minimum: 0
                          max:
                            type: integer
                            minimum: 1
                          accept:
                            type: array
                            items:
                              type: string
                              enum:
                                - image/jpeg
                                - image/png
                                - image/webp
                                - image/heic
                                - image/heif
                            minItems: 1
                            default:
                              - image/jpeg
                              - image/png
                              - image/webp
                              - image/heic
                              - image/heif
                          max_size_bytes:
                            type: integer
                            minimum: 1
                            maximum: 26214400
                            default: 10485760
                        required:
                          - key
                          - type
                          - min
                          - max
                        additionalProperties: false
                  minItems: 1
                expires_in_seconds:
                  type: integer
                  minimum: 60
                  maximum: 86400
                  default: 3600
                status:
                  type: string
                  enum:
                    - draft
                    - active
                  default: active
                metadata:
                  type: object
                  additionalProperties:
                    type: string
                    maxLength: 500
                expected_location:
                  type: string
                  minLength: 1
                  maxLength: 300
                expected_object:
                  type: string
                  minLength: 1
                  maxLength: 200
                condition_aspects:
                  type: array
                  items:
                    type: string
                    minLength: 1
                    maxLength: 40
                  minItems: 1
                  maxItems: 6
                expected_info:
                  type: object
                  properties:
                    shared:
                      type: object
                      properties:
                        expected_location:
                          type: string
                          minLength: 1
                          maxLength: 300
                        expected_object:
                          type: string
                          minLength: 1
                          maxLength: 200
                        condition_aspects:
                          type: array
                          items:
                            type: string
                            minLength: 1
                            maxLength: 40
                          minItems: 1
                          maxItems: 6
                        ocr_type:
                          type: string
                          minLength: 1
                          maxLength: 40
                      additionalProperties: false
                  required:
                    - shared
                  additionalProperties: false
                daily_cap:
                  type: integer
                  minimum: 1
                  maximum: 10000
                total_cap:
                  type: integer
                  minimum: 1
                  maximum: 1000000
                brand_slug:
                  type: string
                  minLength: 2
                  maxLength: 40
                  pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
                identification:
                  type: object
                  properties:
                    required:
                      type: boolean
                    label:
                      type: string
                      minLength: 1
                      maxLength: 80
                  required:
                    - required
                  additionalProperties: false
                database:
                  type: object
                  properties:
                    database_id:
                      type: string
                      pattern: ^[A-Za-z0-9_-]{1,64}$
                    column_for:
                      type: object
                      properties:
                        identifier:
                          type: string
                          minLength: 1
                          maxLength: 80
                        location:
                          type: string
                          minLength: 1
                          maxLength: 80
                        object:
                          type: string
                          minLength: 1
                          maxLength: 80
                      required:
                        - identifier
                      additionalProperties: false
                  required:
                    - database_id
                    - column_for
                  additionalProperties: false
              required:
                - slug
                - name
                - steps
              additionalProperties: false
            example:
              slug: warehouse-intake
              name: Warehouse intake
              mode: capture
              steps:
                - key: front
                  type: image
                  description: Front of the pallet
                  min: 1
                  max: 1
              expires_in_seconds: 3600
      responses:
        '201':
          description: Template published. `version` is always `1` on create.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkTemplate'
              example:
                slug: warehouse-intake
                name: Warehouse intake
                mode: capture
                steps:
                  - key: front
                    type: image
                    description: Front of the pallet
                    min: 1
                    max: 1
                expires_in_seconds: 3600
                status: active
                started_total: 0
                started_today: 0
                version: 1
                created_at: '2026-07-30T12:00:00.000Z'
                updated_at: '2026-07-30T12:00:00.000Z'
        '400':
          description: >-
            Body failed validation (slug charset, steps not mintable, mode
            incoherent).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Slug already taken by a live template of this tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                      - slug_taken
                required:
                  - error
              example:
                error: slug_taken
        '422':
          description: >-
            `brand_slug` references a brand profile that does not exist for the
            tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum:
                      - unknown_brand
                required:
                  - error
              example:
                error: unknown_brand
        '503':
          description: The template store is unreachable — retry with backoff.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  cause:
                    type: string
                required:
                  - error
              example:
                error: storage_unavailable
      security:
        - cookieSession: []
components:
  schemas:
    LinkTemplate:
      type: object
      properties:
        slug:
          type: string
        name:
          type: string
        mode:
          type: string
          enum:
            - capture
            - audit
            - hybrid
        steps:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              type:
                type: string
                enum:
                  - image
                  - upload
                  - text
            required:
              - key
              - type
            description: One normalized capture-spec step (see `stepsInputSchema`).
        expires_in_seconds:
          type: integer
        metadata:
          type: object
          additionalProperties:
            type: string
        expected_info:
          type: object
          properties: {}
        expected_location:
          type: object
          properties: {}
        expected_object:
          type: object
          properties: {}
        condition_aspects:
          type: array
          items: {}
        brand_slug:
          type: string
        identification:
          type: object
          properties:
            required:
              type: boolean
            label:
              type: string
          required:
            - required
        database:
          type: object
          properties:
            database_id:
              type: string
            column_for:
              type: object
              properties:
                identifier:
                  type: string
                location:
                  type: string
                object:
                  type: string
              required:
                - identifier
          required:
            - database_id
            - column_for
        status:
          type: string
          enum:
            - active
            - paused
        daily_cap:
          type: integer
        total_cap:
          type: integer
        started_total:
          type: integer
        started_today:
          type: integer
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - slug
        - name
        - mode
        - steps
        - expires_in_seconds
        - status
        - started_total
        - started_today
        - version
        - created_at
        - updated_at
    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.

````