openapi: 3.1.0
info:
  title: SignaTrust Public API
  description: >-
    Public API for the SignaTrust blockchain-secured document signing platform.
    This specification covers the versioned public API (`/api/v1/`) available
    to third-party developers. Authenticate with an API key via the
    `x-api-key` header. Keys are scoped to specific permissions
    (e.g., `envelopes:read`, `documents:write`) and can be generated from the
    SignaTrust dashboard under Settings > API Keys. Requests are rate-limited
    per key — exceeding the limit returns `429 Too Many Requests`. All errors
    use [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) Problem Details
    format. Response interfaces are backward-compatible — fields may be added
    but never removed. Breaking changes will result in a new version
    (`/api/v2/`).
  version: 1.0.0
  contact:
    name: SignaTrust Support
    url: https://signatrust.io
  license:
    name: Proprietary
    url: https://signatrust.io/terms

servers:
  - url: /
    description: Current environment

security:
  - ApiKeyAuth: []

tags:
  - name: Authentication
    description: Verify an API key and inspect its scopes
  - name: Documents
    description: Upload and download document files
  - name: Envelopes
    description: Create and manage signature envelopes
  - name: Health
    description: Service health check endpoints (no authentication required)
  - name: Signers
    description: Manage signers on an envelope
  - name: Templates
    description: Create, manage, and browse reusable document templates
  - name: Verification
    description: Verify blockchain-anchored transactions (no authentication required)
  - name: Webhooks
    description: >-
      Manage webhook subscriptions for event notifications.


      **Delivery is at-least-once — you must deduplicate.** An event is written
      durably before any attempt is made, then retried on a bounded schedule
      (3 attempts, 1/5/15 minutes apart) before moving to a dead-letter state.
      A delivery that succeeds after your endpoint already processed it — a
      timeout on your side, a retry crossing a slow 200 — will be sent again.


      Deduplicate on the **`X-SignaTrust-Delivery`** header. It carries the
      event id (`evt_<32 hex chars>`) and is **stable across every retry of the
      same event**, so it is the correct idempotency key for your handler.
      Treat a delivery whose id you have already processed as a no-op and
      return 2xx; returning a 4xx will dead-letter the event.


      Do not deduplicate on the request body or on a hash of it. The body is
      byte-identical across retries today, but the **signature is not** — see
      the `X-SignaTrust-Signature` note below.


      Headers on every delivery:


      - `X-SignaTrust-Delivery` — event id; stable across retries. **Dedup on this.**

      - `X-SignaTrust-Event` — event type, e.g. `envelope.completed`.

      - `X-SignaTrust-Signature` — `t=<unix seconds>,v1=<hmac sha256 hex>`, computed
        over `<t>.<raw body>` with your webhook secret. **`t` is the time of that
        attempt, not the time the event was created**, and the signature is
        regenerated for each retry. If you enforce a freshness window on `t`
        (recommended, as a replay defence), a retry arriving 15 minutes after the
        event will still validate — verify against `t`, never against the event
        body's `created` field.

paths:
  # ===========================================================================
  # Documents
  # ===========================================================================
  /api/v1/documents/{id}/download:
    get:
      tags:
        - Documents
      summary: Get document download URL
      description: |
        Returns a pre-signed download URL for the document.
        The URL expires after 1 hour.
      operationId: getDocumentDownloadUrl
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Document ID
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Download URL generated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentDownloadResponse"
        "400":
          description: Document has no associated file
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Document not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /api/v1/documents:
    get:
      tags:
        - Documents
      summary: List your documents
      description: |
        Lists the caller's own documents, most-recent first — each with an `id`
        to pass to create_envelope. Useful for finding a document uploaded via the
        dashboard when you don't already have its id. Optional `name` filter
        (case-insensitive substring).
      operationId: listDocuments
      parameters:
        - name: name
          in: query
          required: false
          schema:
            type: string
          description: Case-insensitive substring to filter documents by name.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: Page number (1-indexed).
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            enum: [10, 25, 50]
          description: Results per page.
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: A page of the caller's documents
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedDocuments"
        "400":
          description: Invalid pagination parameter
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/documents/upload:
    post:
      tags:
        - Documents
      summary: Upload a document
      description: |
        Two ways to upload. RECOMMENDED (one call): pass the file bytes as
        base64 in `content` — the server stores them and returns the document
        record (no `uploadUrl`). Otherwise omit `content` and `size` is required:
        you get back a pre-signed `uploadUrl` to PUT the file to yourself with
        the same `contentType`.
      operationId: uploadDocument
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentUploadRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "201":
          description: Document created with upload URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentUploadResponse"
        "400":
          description: Invalid request (missing name, content type, or size)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ===========================================================================
  # Envelopes
  # ===========================================================================
  /api/v1/envelopes:
    get:
      tags:
        - Envelopes
      summary: List envelopes
      description: |
        Returns a paginated list of envelopes owned by the authenticated user.
        Use query parameters to filter by status or search by name.
      operationId: listEnvelopes
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          schema:
            type: integer
            enum: [10, 25, 50, 100]
            default: 25
          description: Number of items per page
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/EnvelopeStatus"
          description: Filter by envelope status
        - name: search
          in: query
          schema:
            type: string
          description: Search by envelope name (case-insensitive)
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Paginated list of envelopes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelopes"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Envelopes
      summary: Create and send an envelope
      description: |
        Creates a new envelope with documents and signers, then immediately
        sends signing invitations to the first routing-order signers via
        their configured delivery method (email, SMS, or both).
        Requires at least one pre-uploaded document and one signer.
        Alternatively, provide a `templateId` to create from a template.
      operationId: createEnvelope
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEnvelopeRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "201":
          description: Envelope created and sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeDetailResponse"
        "400":
          description: Invalid request or plan limit exceeded
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: One or more documents not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /api/v1/envelopes/{id}:
    get:
      tags:
        - Envelopes
      summary: Get envelope details
      description: |
        Returns full envelope details including documents and signers.
      operationId: getEnvelope
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Envelope details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeDetailResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    patch:
      tags:
        - Envelopes
      summary: Update an envelope
      description: |
        Updates envelope name and/or message.
        Cannot update envelopes with status COMPLETED or VOIDED.
      operationId: updateEnvelope
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEnvelopeRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Envelope updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeDetailResponse"
        "400":
          description: Invalid request or envelope cannot be updated
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    delete:
      tags:
        - Envelopes
      summary: Delete an envelope
      description: |
        Soft-deletes an envelope. Cannot delete envelopes with status
        COMPLETED or SENT — void the envelope first.
      operationId: deleteEnvelope
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Envelope deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeletedResponse"
        "400":
          description: Envelope cannot be deleted in its current status
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /api/v1/envelopes/{id}/void:
    post:
      tags:
        - Envelopes
      summary: Void an envelope
      description: |
        Voids an envelope that is in progress (SENT, VIEWED, or NEEDS_SIGNATURE state).
        The envelope's status is set to VOIDED, all signers receive a cancellation
        notice, an ENVELOPE_VOIDED audit event is written, and an `envelope.voided`
        webhook is dispatched.

        Idempotent at the error level: calling void on an already-voided envelope
        returns 400 without re-firing notifications, audit, or webhook.

        Voided envelopes can subsequently be deleted via `DELETE /api/v1/envelopes/{id}`.
      operationId: voidEnvelope
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/EnvelopeId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Optional reason for voiding, surfaced in notifications, audit event metadata, and webhook payload.
                  maxLength: 500
                  example: "Contract terms changed; sending a revised version."
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Envelope successfully voided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeDetailResponse"
        "400":
          description: Envelope cannot be voided (already COMPLETED or already VOIDED)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: [envelopes:write]

  /api/v1/envelopes/{id}/analyze:
    post:
      tags:
        - Envelopes
      summary: Analyze an envelope's document with AI
      description: |
        Runs AI contract analysis on the envelope's first document and returns the
        result. Requires a plan with AI analysis enabled.
      operationId: analyzeEnvelope
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Analysis result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalysisResponse"
        "400":
          description: Envelope has no document, or AI analysis is not available on this plan
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "502":
          description: The AI provider returned an error
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Storage service unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: [ai:analyze]

  /api/v1/envelopes/{id}/blockchain:
    get:
      tags:
        - Envelopes
      summary: Get blockchain anchor status
      description: |
        Returns the blockchain anchor for an envelope — transaction ID, network,
        explorer URL, and the document hashes. Fields are null until the envelope is
        completed and anchored.
      operationId: getEnvelopeBlockchain
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Blockchain anchor status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlockchainResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: [envelopes:read]

  /api/v1/envelopes/{id}/evidence:
    get:
      tags:
        - Envelopes
      summary: Get the evidence bundle
      description: |
        Returns a self-contained evidence bundle: the envelope, its signers, the full
        audit trail, and the blockchain verification — everything needed to
        independently verify the signature, in one request.
      operationId: getEnvelopeEvidence
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Evidence bundle
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EvidenceBundleResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: [envelopes:read]

  /api/v1/envelopes/{id}/signers:
    get:
      tags:
        - Signers
      summary: List signers
      description: |
        Returns all signers for an envelope, ordered by routing order.
      operationId: listSigners
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: List of signers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SignerResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    post:
      tags:
        - Signers
      summary: Add a signer
      description: |
        Adds a signer to an envelope. Cannot add signers to envelopes
        with status COMPLETED or VOIDED. At least one of email or phone
        is required.
      operationId: addSigner
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/EnvelopeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddSignerRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "201":
          description: Signer added
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignerResponse"
        "400":
          description: |
            Invalid request, envelope in terminal status,
            or max signers reached
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /api/v1/envelopes/{id}/signers/{signerId}:
    delete:
      tags:
        - Signers
      summary: Remove a signer
      description: |
        Removes a signer from an envelope. Cannot remove signers who have
        already signed or from envelopes with status COMPLETED or VOIDED.
      operationId: removeSigner
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
        - name: signerId
          in: path
          required: true
          schema:
            type: string
          description: Signer ID
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Signer removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeletedResponse"
        "400":
          description: Signer has already signed or envelope in terminal status
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope or signer not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /api/v1/envelopes/{id}/signers/{signerId}/signing-url:
    post:
      tags:
        - Signers
      summary: Mint an embedded signing URL
      description: |
        Returns a short-lived, single-use URL that drops a signer straight into the
        signing experience — for embedding inside your own application (an iframe, or
        a redirect/popup) instead of waiting for the emailed link.

        The URL carries a 30-minute, single-use embed token, not the signer's durable
        access token, so your backend never handles the long-lived credential. The
        first request to the URL exchanges the embed token for the signing session;
        a replayed URL lands on the signing page with no session.

        The envelope's security level must be VERIFIED or CERTIFIED. STANDARD
        envelopes are rejected: STANDARD attribution relies on possession of the
        emailed link, which an embedded session bypasses, so an in-ceremony identity
        check (one-time passcode or passkey) is required. The origination is recorded
        as an `EMBED_SIGNING_URL_GENERATED` audit event.

        To render the URL in an iframe, the embedding origin must be allow-listed by
        SignaTrust; otherwise the browser blocks cross-origin framing.
      operationId: createSigningUrl
      parameters:
        - $ref: "#/components/parameters/EnvelopeId"
        - name: signerId
          in: path
          required: true
          schema:
            type: string
          description: Signer ID
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Signing URL minted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SigningUrlResponse"
        "400":
          description: |
            Envelope is STANDARD-tier, not awaiting signature, or the signer
            has already signed or declined.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Envelope or signer not found, or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: [signing:embed]

  # ===========================================================================
  # Templates
  # ===========================================================================
  /api/v1/templates:
    get:
      tags:
        - Templates
      summary: List templates
      description: |
        Returns a paginated list of templates available to the authenticated user.
        Includes the user's own templates and system marketplace templates.
        Use query parameters to filter by vertical, category, or search by name.
      operationId: listTemplates
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          schema:
            type: integer
            enum: [10, 25, 50]
            default: 10
          description: Number of items per page
        - name: vertical
          in: query
          schema:
            type: string
          description: Filter by vertical (e.g., "real_estate")
        - name: category
          in: query
          schema:
            type: string
          description: Filter by category (case-insensitive contains)
        - name: search
          in: query
          schema:
            type: string
          description: Search by template name (case-insensitive contains)
        - name: featured
          in: query
          schema:
            type: string
            enum: ["true"]
          description: Filter to featured templates only
        - name: isSystem
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Filter to system or user templates only
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Paginated list of templates
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedTemplates"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Templates
      summary: Create a template
      description: |
        Creates a new template record and returns a pre-signed upload URL.
        Upload the document file to the returned `uploadUrl` using a PUT
        request with the same content type. Templates created via the API
        are always user-owned (`isSystem: false`).
      operationId: createTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTemplateRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "201":
          description: Template created with upload URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateCreateResponse"
        "400":
          description: Invalid request (missing name or documentName)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/templates/{id}:
    get:
      tags:
        - Templates
      summary: Get template details
      description: |
        Returns full template details including field definitions,
        signer role definitions, and a pre-signed document download URL
        (expires after 1 hour). Only returns system templates or templates
        owned by the authenticated user.
      operationId: getTemplate
      parameters:
        - $ref: "#/components/parameters/TemplateId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Template details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateDetailResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Template not found or not accessible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    patch:
      tags:
        - Templates
      summary: Update a template
      description: |
        Updates a user-owned template. System templates cannot be modified.
        Only the template owner can update it. Returns the updated template
        with a pre-signed document URL.
      operationId: updateTemplate
      parameters:
        - $ref: "#/components/parameters/TemplateId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTemplateRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Template updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateDetailResponse"
        "400":
          description: Invalid request or system template
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Template not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    delete:
      tags:
        - Templates
      summary: Delete a template
      description: |
        Soft-deletes a user-owned template. System templates cannot be
        deleted. Only the template owner can delete it. Deleted templates
        no longer appear in list results.
      operationId: deleteTemplate
      parameters:
        - $ref: "#/components/parameters/TemplateId"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Template deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeletedResponse"
        "400":
          description: System template cannot be deleted
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Template not found or not owned by caller
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /api/v1/templates/{id}/clone:
    post:
      tags:
        - Templates
      summary: Clone a template into a document
      description: |
        Server-side clones a template — system, organization, or your own — into a new
        document you own, ready to attach to an envelope. Optionally rename it. The
        copy happens on the server so the source file is never exposed.
      operationId: cloneTemplate
      parameters:
        - $ref: "#/components/parameters/TemplateId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 256
                  description: Optional name for the cloned document.
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "201":
          description: Template cloned into a new document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentResponse"
        "400":
          description: Invalid body, or the template has no stored file to copy
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Template not found or not accessible
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Storage service unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: [documents:write]

  # ===========================================================================
  # Health
  # ===========================================================================
  /api/v1/healthz:
    get:
      tags:
        - Health
      summary: Liveness check
      description: |
        Returns 200 if the API is alive and running.
        Use this to verify connectivity.
      operationId: livenessCheck
      security: []
      responses:
        "200":
          description: API is alive
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                status: ok
                timestamp: "2026-02-10T12:00:00.000Z"

  /api/v1/readyz:
    get:
      tags:
        - Health
      summary: Readiness check
      description: |
        Returns 200 if the API is ready to accept requests.
        Checks database connectivity. Returns 503 if the database
        is unavailable.
      operationId: readinessCheck
      security: []
      responses:
        "200":
          description: API is ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadinessResponse"
              example:
                status: ok
                checks:
                  database:
                    ready: true
                    provider: postgresql
                timestamp: "2026-02-10T12:00:00.000Z"
        "503":
          description: One or more dependencies are unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadinessResponse"
              example:
                status: degraded
                checks:
                  database:
                    ready: false
                    provider: postgresql
                timestamp: "2026-02-10T12:00:00.000Z"

  # ===========================================================================
  # Verification
  # ===========================================================================
  /api/v1/verify/{txId}:
    get:
      tags:
        - Verification
      summary: Verify a blockchain transaction
      description: |
        Verifies a document's blockchain anchor by looking up the
        transaction on the Solana blockchain. No authentication required.
      operationId: verifyTransaction
      security: []
      parameters:
        - name: txId
          in: path
          required: true
          schema:
            type: string
          description: Blockchain transaction ID
      responses:
        "200":
          description: Transaction verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerificationResponse"
        "404":
          description: Transaction not found or blockchain not configured
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  # ===========================================================================
  # Authentication
  # ===========================================================================
  /api/v1/auth/verify:
    get:
      tags:
        - Authentication
      summary: Verify the API key
      description: |
        Returns the authenticated account's email, name, plan, and the scopes granted
        to the API key. Use it to confirm a key is valid and see what it can do.
      operationId: verifyApiKey
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: The key is valid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthVerifyResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Account not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
      security:
        - apiKey: []

  # ===========================================================================
  # Webhooks
  # ===========================================================================
  /api/v1/webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhooks
      description: |
        Returns all webhook subscriptions for the authenticated user.
      operationId: listWebhooks
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: List of webhooks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WebhookResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Webhooks
      summary: Create a webhook
      description: |
        Creates a webhook subscription. The response includes a `secret`
        field that is only shown once — store it securely to verify
        webhook signatures.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookRequest"
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "201":
          description: Webhook created (includes secret shown only once)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookCreateResponse"
        "400":
          description: Invalid URL or event types
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/webhooks/{id}:
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook
      description: |
        Deletes a webhook subscription. Events will no longer be
        delivered to the webhook URL.
      operationId: deleteWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Webhook ID
      responses:
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeletedResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Webhook not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        API key for authenticating public API requests.
        Generate keys from the SignaTrust dashboard (Settings > API Keys), or — for a
        key the organization owns — from Organization Settings.

        A key is owned either by a **user** or by an **organization**. An organization
        key keeps working when the person who created it leaves, which is what makes it
        the right choice for a partner integration.

        Requests made with an organization API key are attributed to the organization's
        current owner.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: |
        Opt-in retry safety. Supply a unique value (a UUID is the usual choice) and
        the first response for that key is cached for **7 days**; a repeat request
        with the same key AND the same body returns those exact bytes instead of
        performing the write again. Retry a timed-out or network-interrupted call
        with the same key and you get one envelope, not two.

        Replays carry `Idempotent-Replayed: true`.

        **Reusing a key with a different body is a `409`, not a replay.** The body is
        fingerprinted when the key is first used. A repeat request whose body does not
        match that fingerprint is refused, and the refusal names the first twelve
        characters of the stored fingerprint so you can tell which earlier request you
        collided with. The stored body itself is never echoed back. Use a fresh key for
        a genuinely new request; resend the original body to replay the original
        response.

        The fingerprint covers the raw bytes you sent, so a payload re-serialised
        differently on retry — different key order, different whitespace — does not
        match and is refused. Send the same bytes you sent the first time.

        **Scope.** Keys are scoped to whoever made the call: the signer for the signing
        plane, the ORGANIZATION for a request authenticated with an organization API
        key, and the user otherwise. An organization key's replays therefore survive a
        change of owner. A key you choose can never collide with another customer's.

        Responses with status < 500 are cached, including 4xx — a request that was
        rejected as malformed stays rejected on retry. 5xx responses are NOT cached,
        so a server-side failure can be retried for a fresh outcome. Keys longer
        than 255 characters or containing control characters are rejected with 400.

        Keys stored before body fingerprinting shipped carry no fingerprint and replay
        as they always did, rather than being refused on a comparison that cannot be
        made.

    EnvelopeId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Envelope ID

    TemplateId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Template ID

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    TooManyRequests:
      description: |
        A rate limit was exceeded. THREE separate ceilings can produce this, and the
        `detail` field says which:

        - **Per key.** Every API key has its own bucket. One key running hot cannot
          consume another key's allowance.
        - **Per organization.** Keys owned by an organization additionally share one
          ceiling across the whole firm, sized by the organization's API tier. A firm
          cannot raise its total simply by minting more keys. The response says "Your
          organization has exceeded its rate limit"; it does not quote a number.
        - **Per IP.** The counterparty surface is additionally bounded per client
          address.

        A request must pass every ceiling that applies to it.

        `Retry-After: 60` is sent on every one of these responses. There are no
        `X-RateLimit-*` headers — the API does not currently expose remaining rate-limit
        allowance. `X-Quota-*` headers are a different thing: they report the MONTHLY
        envelope quota, not these per-minute ceilings.
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Always `60` today.
          schema:
            type: integer
            example: 60
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

  schemas:
    AddSignerRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Signer's full name
        email:
          type: string
          format: email
          description: Email address (required if phone is not provided)
        phone:
          type: string
          description: Phone number (required if email is not provided)
        role:
          $ref: "#/components/schemas/SignerRole"
        routingOrder:
          type: integer
          minimum: 1
          default: 1
          description: Signing order (lower numbers sign first)
        deliveryMethod:
          $ref: "#/components/schemas/DeliveryMethod"

    BlockchainInfo:
      type: object
      required:
        - txId
        - network
        - explorerUrl
        - timestamp
      properties:
        txId:
          type:
            - string
            - "null"
          description: Blockchain transaction ID
        network:
          type:
            - string
            - "null"
          description: Blockchain network (e.g., "devnet", "mainnet-beta")
        explorerUrl:
          type:
            - string
            - "null"
          format: uri
          description: URL to view the transaction on a block explorer
        timestamp:
          type:
            - string
            - "null"
          format: date-time
          description: Timestamp when the transaction was recorded

    CreateEnvelopeRequest:
      type: object
      required:
        - name
        - documentIds
        - signers
      properties:
        name:
          type: string
          maxLength: 256
          description: Envelope name
        message:
          type: string
          description: Optional message to include with the envelope
        documentIds:
          type: array
          items:
            type: string
          minItems: 1
          description: |
            Array of document IDs to attach. Documents must be pre-uploaded
            via `POST /api/v1/documents/upload`.
        signers:
          type: array
          items:
            $ref: "#/components/schemas/CreateSignerInput"
          minItems: 1
          description: Signers to add to the envelope
        templateId:
          type: string
          description: |
            Optional template ID to create from a template. When provided,
            `documentIds` is not required (the template document is copied).
        variables:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: integer
          description: |
            Values for a standard template's declared variables, validated
            against that template's own variables schema. Only a standard
            template accepts variables; each invalid value is reported as its
            own entry in `errors`. A standard template whose stored document no
            longer matches its published hash is refused with `409` and problem
            type `https://signatrust.io/problems/standard-body-mismatch`.
        organizationId:
          type: string
          description: |
            Attribute the envelope to one of your organizations. Omit it and
            the envelope is filed under the key owner's organization when they
            belong to exactly one — the same rule the dashboard applies — and
            is personal when they belong to none or to several; supply it to
            choose explicitly. Send `null` to create a personal envelope even
            when the owner belongs to one organization. Attribution applies
            the organization's signing-surface branding (logo and accent) and
            sets the email From-line to "{organization name} via SignaTrust",
            with the reply-to set to the sender's address. It does not change
            where documents are stored. Requesting an organization the key
            owner is not a current member of returns 404. Organization
            envelopes are visible to the owner's colleagues under the
            organization's visibility policy, on the dashboard and on this API.
        fields:
          type: array
          items:
            $ref: "#/components/schemas/FieldPlacementInput"
          description: |
            Optional field placements on the first document. Use `signerIndex`
            (0-based) to assign fields to specific signers.

    CreateSignerInput:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Signer's full name
        email:
          type: string
          format: email
          description: Signer's email address (required if no phone)
        phone:
          type: string
          description: Signer's phone number (required if no email)
        role:
          type: string
          enum: [SIGNER, OBSERVER]
          default: SIGNER
          description: Signer role
        routingOrder:
          type: integer
          minimum: 1
          description: Signing order (1 = first, 2 = second). Defaults to array index + 1.
        deliveryMethod:
          type: string
          enum: [EMAIL, SMS, BOTH, WHATSAPP]
          default: EMAIL
          description: How to deliver the signing invitation

    FieldPlacementInput:
      type: object
      required:
        - type
        - x
        - y
      properties:
        type:
          type: string
          enum: [SIGNATURE, INITIALS, DATE, TEXT, CHECKBOX, FULL_NAME]
          description: Field type
        documentIndex:
          type: integer
          minimum: 0
          default: 0
          description: 0-based index into the documentIds array (defaults to first document)
        signerIndex:
          type: integer
          minimum: 0
          description: 0-based index into the signers array
        pageNumber:
          type: integer
          minimum: 1
          default: 1
          description: Page number (1-indexed)
        x:
          type: number
          description: X position as percentage of page width (0-100)
        y:
          type: number
          description: Y position as percentage of page height (0-100)
        width:
          type: number
          default: 30
          description: Field width as percentage of page width
        height:
          type: number
          default: 5
          description: Field height as percentage of page height
        label:
          type: string
          description: Optional label for the field
        required:
          type: boolean
          default: true
          description: Whether this field is required

    CreateTemplateRequest:
      type: object
      required:
        - name
        - documentName
      properties:
        name:
          type: string
          maxLength: 256
          description: Template name
        documentName:
          type: string
          description: Original document file name (e.g., "contract.pdf")
        contentType:
          type: string
          default: application/pdf
          description: MIME type of the document (defaults to "application/pdf")
        description:
          type: string
          description: Template description
        category:
          type: string
          description: Template category (e.g., "Real Estate")
        vertical:
          type: string
          description: Industry vertical (e.g., "real_estate")
        tags:
          type: array
          items:
            type: string
          description: Tags for template discovery

    CreateWebhookRequest:
      type: object
      required:
        - name
        - url
        - events
      properties:
        name:
          type: string
          description: Webhook name
        url:
          type: string
          format: uri
          description: HTTPS URL to receive webhook events
        events:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/WebhookEventType"
          description: Event types to subscribe to

    DeletedResponse:
      type: object
      required:
        - id
        - deleted
      properties:
        id:
          type: string
        deleted:
          type: boolean
          const: true

    DeliveryMethod:
      type: string
      enum:
        - EMAIL
        - SMS
        - BOTH

    DocumentDownloadResponse:
      type: object
      required:
        - id
        - name
        - downloadUrl
        - expiresIn
      properties:
        id:
          type: string
        name:
          type: string
        downloadUrl:
          type: string
          format: uri
          description: Pre-signed download URL (expires after `expiresIn` seconds)
        expiresIn:
          type: integer
          description: Seconds until the download URL expires

    DocumentResponse:
      type: object
      required:
        - id
        - name
        - contentType
        - size
        - hash
        - createdAt
      properties:
        id:
          type: string
        name:
          type: string
        contentType:
          type: string
        size:
          type: integer
          description: File size in bytes
        hash:
          type:
            - string
            - "null"
          description: SHA-256 hash of the file content
        createdAt:
          type: string
          format: date-time

    DocumentUploadRequest:
      type: object
      required:
        - name
        - contentType
      properties:
        name:
          type: string
          description: File name
        contentType:
          type: string
          description: MIME type (e.g., "application/pdf")
        content:
          type: string
          description: |
            Base64-encoded file bytes. Provide this to upload in ONE call — the
            server stores the bytes and the response omits `uploadUrl`. A `data:`
            URI is accepted. Omit to use the two-step presigned flow (then `size`
            is required).
        expectedHash:
          type: string
          pattern: "^[a-fA-F0-9]{64}$"
          description: |
            Optional sha256 (hex) of the file you intend to send. When present with
            `content`, the server verifies the decoded bytes against it and rejects
            with 400 on mismatch (no object, no record stored) — transmission
            integrity enforced by the API rather than caller diligence.
        size:
          type: integer
          description: |
            File size in bytes. Required only when `content` is omitted
            (presigned flow); ignored on the inline path, where the server uses
            the real decoded length.
          minimum: 1

    DocumentUploadResponse:
      allOf:
        - $ref: "#/components/schemas/DocumentResponse"
        - type: object
          properties:
            uploadUrl:
              type: string
              format: uri
              description: |
                Pre-signed upload URL — PUT the file to this URL with the same
                Content-Type. Present only on the two-step flow; OMITTED when the
                document was uploaded inline via `content` (upload already
                complete — `hash` and `size` carry the stored values).

    EnvelopeDetailResponse:
      allOf:
        - $ref: "#/components/schemas/EnvelopeResponse"
        - type: object
          required:
            - documents
            - signers
          properties:
            documents:
              type: array
              items:
                $ref: "#/components/schemas/DocumentResponse"
            signers:
              type: array
              items:
                $ref: "#/components/schemas/SignerResponse"

    EnvelopeResponse:
      type: object
      required:
        - id
        - name
        - status
        - message
        - createdAt
        - updatedAt
        - sentAt
        - completedAt
        - voidedAt
        - voidReason
        - blockchain
      properties:
        id:
          type: string
        name:
          type: string
        status:
          $ref: "#/components/schemas/EnvelopeStatus"
        message:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        sentAt:
          type:
            - string
            - "null"
          format: date-time
        completedAt:
          type:
            - string
            - "null"
          format: date-time
        voidedAt:
          type:
            - string
            - "null"
          format: date-time
        voidReason:
          type:
            - string
            - "null"
        blockchain:
          $ref: "#/components/schemas/BlockchainInfo"

    EnvelopeStatus:
      type: string
      enum:
        - SENT
        - NEEDS_SIGNATURE
        - COMPLETED
        - VOIDED
        - DECLINED

    FieldDef:
      type: object
      required:
        - type
        - pageNumber
        - x
        - "y"
        - width
        - height
        - signerIndex
      properties:
        type:
          type: string
          description: Field type (e.g., "SIGNATURE", "INITIALS", "DATE")
        pageNumber:
          type: integer
          description: Page number where the field appears (1-based)
        x:
          type: number
          description: Horizontal position (percentage of page width)
        "y":
          type: number
          description: Vertical position (percentage of page height)
        width:
          type: number
          description: Field width (percentage of page width)
        height:
          type: number
          description: Field height (percentage of page height)
        signerIndex:
          type: integer
          description: Index of the signer role this field is assigned to

    HealthResponse:
      type: object
      required:
        - status
        - timestamp
      properties:
        status:
          type: string
          const: ok
          description: Always "ok" when the service is alive
        timestamp:
          type: string
          format: date-time

    PaginatedEnvelopes:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/EnvelopeResponse"
        pagination:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedDocuments:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/DocumentResponse"
        pagination:
          $ref: "#/components/schemas/PaginationMeta"

    PaginatedTemplates:
      type: object
      required:
        - data
        - pagination
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/TemplateResponse"
        pagination:
          $ref: "#/components/schemas/PaginationMeta"

    PaginationMeta:
      type: object
      required:
        - hasNext
        - hasPrev
        - limit
        - page
        - total
        - totalPages
      properties:
        hasNext:
          type: boolean
        hasPrev:
          type: boolean
        limit:
          type: integer
        page:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer

    ProblemDetails:
      type: object
      description: |
        RFC 7807 compliant error response format.
        All API error responses use this format with
        Content-Type: application/problem+json.
      required:
        - status
        - title
        - type
      properties:
        detail:
          type: string
          description: Human-readable explanation of this occurrence
        instance:
          type: string
          format: uri-reference
          description: URI identifying this specific occurrence
        status:
          type: integer
          description: HTTP status code
        title:
          type: string
          description: Short, human-readable summary of the problem
        type:
          type: string
          format: uri
          description: URI identifying the problem type

    ReadinessResponse:
      type: object
      required:
        - status
        - checks
        - timestamp
      properties:
        status:
          type: string
          enum: [ok, degraded]
        checks:
          $ref: "#/components/schemas/ServiceChecks"
        timestamp:
          type: string
          format: date-time

    ServiceCheck:
      type: object
      required:
        - ready
        - provider
      properties:
        ready:
          type: boolean
        provider:
          type: string

    ServiceChecks:
      type: object
      required:
        - database
      properties:
        database:
          $ref: "#/components/schemas/ServiceCheck"

    SignerResponse:
      type: object
      required:
        - id
        - name
        - deliveryMethod
        - email
        - phone
        - role
        - routingOrder
        - status
        - signedAt
        - viewedAt
        - declinedAt
      properties:
        id:
          type: string
        name:
          type: string
        deliveryMethod:
          $ref: "#/components/schemas/DeliveryMethod"
        email:
          type:
            - string
            - "null"
        phone:
          type:
            - string
            - "null"
        role:
          $ref: "#/components/schemas/SignerRole"
        routingOrder:
          type: integer
        status:
          $ref: "#/components/schemas/SignerStatus"
        signedAt:
          type:
            - string
            - "null"
          format: date-time
        viewedAt:
          type:
            - string
            - "null"
          format: date-time
        declinedAt:
          type:
            - string
            - "null"
          format: date-time

    SigningUrlResponse:
      type: object
      required:
        - signingUrl
        - expiresAt
        - expiresInSeconds
      properties:
        signingUrl:
          type: string
          description: Short-lived, single-use URL that drops the signer into the signing experience.
          example: "https://app.signatrust.io/sign/embed/env_abc?et=eyJ..."
        expiresAt:
          type: string
          format: date-time
          description: When the embed token expires (30 minutes after minting).
        expiresInSeconds:
          type: integer
          description: Seconds until the embed token expires.
          example: 1800

    AuthVerifyResponse:
      type: object
      required:
        - email
        - name
        - plan
        - apiPlan
        - apiAccess
        - scopes
      properties:
        email:
          type: string
        name:
          type:
            - string
            - "null"
        plan:
          type: string
          description: >-
            Retail subscription plan (e.g. FREE, PRO, BUSINESS). NOT a reliable
            signal of API entitlement — an API-tier customer stays FREE here.
            Gate on apiAccess instead.
        apiPlan:
          type:
            - string
            - "null"
          description: >-
            Standalone API tier (e.g. API_STARTER), or null. Set independently
            of plan.
        apiAccess:
          type: boolean
          description: >-
            Whether this key's owner is entitled to the v1 API — true when the
            retail plan is PRO/BUSINESS or any apiPlan is held. Integrators
            should gate on this, not on plan.
        scopes:
          type: array
          items:
            type: string
          description: Permission scopes granted to this API key.

    AnalysisResponse:
      type: object
      required:
        - envelopeId
        - analysis
        - analyzedAt
      properties:
        envelopeId:
          type: string
        analysis:
          type:
            - object
            - "null"
          additionalProperties: true
          description: AI analysis result (summary, risks, unusual clauses), or null if not yet analyzed.
        analyzedAt:
          type:
            - string
            - "null"
          format: date-time

    BlockchainResponse:
      type: object
      required:
        - envelopeId
        - txId
        - network
        - explorerUrl
        - compositeHash
        - verified
        - timestamp
      properties:
        envelopeId:
          type: string
        txId:
          type:
            - string
            - "null"
        network:
          type:
            - string
            - "null"
        explorerUrl:
          type:
            - string
            - "null"
        compositeHash:
          type:
            - string
            - "null"
        fileHash:
          type:
            - string
            - "null"
        hashVersion:
          type: integer
        verified:
          type: boolean
        timestamp:
          type:
            - integer
            - "null"
          description: Unix seconds of the anchor, or null if not anchored.

    EvidenceBundleResponse:
      type: object
      required:
        - bundle
      properties:
        bundle:
          type: object
          required:
            - envelope
            - signers
            - auditTrail
          properties:
            envelope:
              $ref: "#/components/schemas/EnvelopeResponse"
            signers:
              type: array
              items:
                $ref: "#/components/schemas/SignerResponse"
            auditTrail:
              type: object
              required:
                - events
                - startTime
                - endTime
              properties:
                events:
                  type: array
                  items:
                    type: object
                    additionalProperties: true
                  description: Ordered audit-log events for the envelope.
                startTime:
                  type: string
                  format: date-time
                endTime:
                  type: string
                  format: date-time
            blockchainVerification:
              $ref: "#/components/schemas/VerificationResponse"
            delegation:
              type: object
              nullable: true
              description: >-
                Present while delegated sending is enabled: on an envelope a delegate sent
                on a principal's behalf, both people and the grant it was sent under; null
                on a send in the sender's own name. Absent when the feature is off.
              required:
                - delegationId
                - actor
                - principal
              properties:
                delegationId:
                  type: string
                  description: The grant the delegated send was made under.
                actor:
                  type: object
                  description: The delegate who sent the envelope.
                  required: [name, email]
                  properties:
                    name:
                      type: string
                      nullable: true
                    email:
                      type: string
                      nullable: true
                principal:
                  type: object
                  description: The person on whose behalf it was sent, and its sender of record.
                  required: [name, email]
                  properties:
                    name:
                      type: string
                      nullable: true
                    email:
                      type: string
                      nullable: true

    SignerRole:
      type: string
      enum:
        - SIGNER
        - OBSERVER

    SignerRoleDef:
      type: object
      required:
        - role
        - order
        - action
      properties:
        role:
          type: string
          description: Display name of the signer role (e.g., "Tenant", "Landlord")
        order:
          type: integer
          description: Signing order for this role
        action:
          type: string
          description: Action expected from this role (e.g., "SIGN", "OBSERVE")

    SignerStatus:
      type: string
      enum:
        - PENDING
        - SENT
        - VIEWED
        - SIGNED
        - DECLINED

    TemplateCreateResponse:
      type: object
      required:
        - id
        - name
        - documentName
        - contentType
        - uploadUrl
        - createdAt
      properties:
        id:
          type: string
        name:
          type: string
        documentName:
          type: string
        contentType:
          type: string
        uploadUrl:
          type: string
          format: uri
          description: |
            Pre-signed upload URL. PUT the document file to this URL
            with the same Content-Type specified in the request.
        createdAt:
          type: string
          format: date-time

    TemplateDetailResponse:
      allOf:
        - $ref: "#/components/schemas/TemplateResponse"
        - type: object
          required:
            - documentName
            - fields
            - documentUrl
          properties:
            documentName:
              type: string
              description: Original document file name
            fields:
              type: array
              items:
                $ref: "#/components/schemas/FieldDef"
            documentUrl:
              type:
                - string
                - "null"
              format: uri
              description: Pre-signed document URL (expires after 1 hour)

    TemplateResponse:
      type: object
      required:
        - id
        - name
        - description
        - category
        - vertical
        - tags
        - contentType
        - fieldCount
        - signerRoles
        - featured
        - isSystem
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type:
            - string
            - "null"
        category:
          type:
            - string
            - "null"
        vertical:
          type:
            - string
            - "null"
        tags:
          type: array
          items:
            type: string
        contentType:
          type: string
        fieldCount:
          type: integer
          description: Number of fields defined in the template
        signerRoles:
          type: array
          items:
            $ref: "#/components/schemas/SignerRoleDef"
        featured:
          type: boolean
        isSystem:
          type: boolean
          description: Whether this is a system marketplace template
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    UpdateEnvelopeRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 256
          description: Envelope name
        message:
          type: string
          description: Optional message to include with the envelope

    UpdateTemplateRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 256
          description: Template name
        description:
          type:
            - string
            - "null"
          description: Template description
        category:
          type:
            - string
            - "null"
          description: Template category
        fields:
          type: array
          items:
            $ref: "#/components/schemas/FieldDef"
          description: Field definitions
        signerRoles:
          type: array
          items:
            $ref: "#/components/schemas/SignerRoleDef"
          description: Signer role definitions

    VerificationResponse:
      type: object
      required:
        - verified
        - transactionId
        - network
        - explorerUrl
      properties:
        verified:
          type: boolean
          const: true
        transactionId:
          type: string
          description: Blockchain transaction ID
        network:
          type: string
          description: Blockchain network
        blockNumber:
          type: integer
          description: Block number containing the transaction
        timestamp:
          type: string
          description: Timestamp from the blockchain
        explorerUrl:
          type: string
          format: uri
          description: URL to view on a block explorer
        data:
          type: string
          description: Data stored in the transaction (document hash)

    WebhookCreateResponse:
      allOf:
        - $ref: "#/components/schemas/WebhookResponse"
        - type: object
          required:
            - secret
          properties:
            secret:
              type: string
              description: |
                Webhook signing secret — shown only once on creation.
                Use this to verify webhook signatures.

    WebhookEventType:
      type: string
      enum:
        - envelope.created
        - envelope.sent
        - envelope.viewed
        - envelope.signed
        - envelope.completed
        - envelope.voided
        - envelope.declined

    WebhookResponse:
      type: object
      required:
        - id
        - name
        - url
        - events
        - isActive
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEventType"
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
