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: 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

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:
        "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/upload:
    post:
      tags:
        - Documents
      summary: Upload a document
      description: |
        Creates a document record and returns a pre-signed upload URL.
        Upload the file to the returned `uploadUrl` using a PUT request
        with the same `contentType`.
      operationId: uploadDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DocumentUploadRequest"
      responses:
        "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:
        "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
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEnvelopeRequest"
      responses:
        "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:
        "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:
        "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:
        "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/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:
        "200":
          description: Envelope successfully voided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvelopeDetail"
        "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}/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:
        "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/EnvelopeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddSignerRequest"
      responses:
        "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:
        "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"

  # ===========================================================================
  # 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:
        "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:
        "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:
        "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:
        "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:
        "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"

  # ===========================================================================
  # 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"

  # ===========================================================================
  # Webhooks
  # ===========================================================================
  /api/v1/webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhooks
      description: |
        Returns all webhook subscriptions for the authenticated user.
      operationId: listWebhooks
      responses:
        "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:
        "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:
        "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).

  parameters:
    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"

  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).
        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
        - size
      properties:
        name:
          type: string
          description: File name
        contentType:
          type: string
          description: MIME type (e.g., "application/pdf")
        size:
          type: integer
          description: File size in bytes
          minimum: 1

    DocumentUploadResponse:
      allOf:
        - $ref: "#/components/schemas/DocumentResponse"
        - type: object
          required:
            - uploadUrl
          properties:
            uploadUrl:
              type: string
              format: uri
              description: |
                Pre-signed upload URL. PUT the file to this URL with the
                same Content-Type specified in the request.

    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"

    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

    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
