openapi: 3.1.0
info:
  title: Tvarka Sign API
  version: 1.2.1
  x-status: >-
    Live and publicly reachable at https://sign-api.tvarka.pro since
    2026-08-27, on both production boxes and behind Cloudflare. The first
    production tenant was onboarded 2026-08-28 and the launch gate
    ceremony ran the same day: a production signing ordered through this
    API was signed with a real eID (Mobile-ID) and validated
    QUALIFIED_VALID, and webhook delivery was exercised the same day
    against an external production receiver with the HMAC signature and
    idempotency key verified on arrival.
  description: |
    Machine channel for Tvarka Sign: post a document and a list of signers,
    and Tvarka runs its ordinary qualified signing ceremony for each of them.

    **What this API is.** Orchestration. The ceremony itself is the shipped
    Tvarka Sign ceremony a person already uses: the same hosted page, the
    same signing methods (Lithuanian ID card over NFC in the Tvarka Sign
    mobile app, Smart-ID and Mobile-ID on the hosted page), the same PIN
    rules, the same invitation lifecycle. Nothing
    here signs anything - a signature is always a human act with their own
    eID, so every endpoint below is about getting a document in front of the
    right person and finding out what they did.

    **Signature formats.** This orchestration API accepts PDF, appendable
    ASiC-E containers, and the ADOC family - `.adoc`, `.bedoc`, `.cedoc`,
    `.gedoc`, `.ggedoc` - for countersigning one that is already signed.
    A container is signed in place, never converted. MDOC is not accepted
    for countersigning: its composer writes a first signature only.
    The complete Tvarka API estate produces every
    Regulation (EU) 2026/248 Annex I family: PAdES, XAdES, CAdES, JAdES and
    ASiC. The ATK API is the lower-level card and reader API, including full
    LT ATK NFC, USB/reader and detached CAdES support. The DD API produces
    JAdES. See the format matrix in the developer guide before selecting a
    surface.

    **Deliberate scope.** Tvarka offers simple, advisory signature
    validation. It does not sell qualified validation and it does not offer
    an electronic-seal API. Those are product boundaries, not launch gaps.

    **Tenancy and billing.** A tenant is a Tvarka entity with a Sign
    workspace, and ceremonies are raised in that entity's name, so the
    counterparty sees who is actually asking. Signatures are billed to that
    workspace on the ordinary per-signature price ladder; there is no
    separate API tariff and no subscription.

    **Sandbox.** A `tsk_test_` key opens real ceremonies but is exempt from
    the funding precondition, and can drive a signing to `completed`,
    `declined` or `expired` through `/simulate`. A simulated completion is
    explicitly marked `sandbox_simulation` and returns a real downloadable
    artifact, so download, hashing and storage can be exercised without a
    real eID. The sandbox never adds a signature: an unsigned PDF comes back
    as the caller's own pages with a banner saying it is not signed, and
    anything posted already signed comes back byte-identical. Every artifact
    is flagged `sandbox: true`. Tvarka issues no test signatures. A
    production completion is always a real human ceremony.
  contact:
    name: Tvarka
    email: info@tvarka.pro
    url: https://tvarka.pro
servers:
  - url: https://sign-api.tvarka.pro
    description: Production and sandbox (selected by the key)

security:
  - bearerAuth: []

tags:
  - name: Signings
    description: Create and follow a signing.

paths:
  /v1/signings:
    get:
      tags: [Signings]
      operationId: listSignings
      summary: List signings
      parameters:
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/SigningStatus' }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: startingAfter
          in: query
          schema: { type: string, format: uuid }
      responses:
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: A newest-first page of signings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SigningList' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
    post:
      tags: [Signings]
      operationId: createSigning
      summary: Create a signing
      description: |
        Stores the document, opens one ceremony per signer, and returns the
        ceremony URLs (or emails them, with `delivery: email`).

        All signers sign the same document and are unordered: whoever opens
        their link first signs first, and each subsequent signer signs the
        output the previous one produced. Two signers never sign at the same
        moment - the ceremony serializes them.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSigningRequest'
      responses:
        '201':
          description: The signing was created and every signer has a live ceremony.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signing' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
        '402':
          description: |
            The tenant cannot pay for the ceremony: no payment rail, an
            overdue invoice, or a tripped usage ceiling. Nothing was
            created and nobody was invited. `recoveryUrl` names the web
            page where the tenant can fix it.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '422':
          description: |
            A well-formed request for a container that cannot take another
            signature - unsigned, already finalized, or structurally
            unsound. Not billed.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }

  /v1/signings/{signingId}:
    get:
      tags: [Signings]
      operationId: getSigning
      summary: Get a signing
      description: Current state of the signing and each of its signers.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The signing.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signing' }
        '404': { $ref: '#/components/responses/Problem' }
    patch:
      tags: [Signings]
      operationId: updateSigning
      summary: Update a live signing deadline
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                expiresInDays: { type: integer, minimum: 1, maximum: 30 }
              required: [expiresInDays]
              additionalProperties: false
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The updated signing.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signing' }
        '400': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
    delete:
      tags: [Signings]
      operationId: eraseSigning
      summary: Erase one signing
      description: |
        Cancels a live signing first, then purges captured identity and
        caller-supplied personal data. For a document uploaded through this
        API, its bytes and vault record are also deleted. If the MCP server
        opened the ceremony on a pre-existing vault document, that shared
        document and its bytes are preserved while the dedicated ceremony is
        removed. A de-identified audit row remains. Idempotent.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: Erasure result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  signingId: { type: string, format: uuid }
                  deleted: { type: boolean, const: true }
                  changed: { type: boolean }
                required: [signingId, deleted, changed]
                additionalProperties: false
        '404': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }

  /v1/batches:
    post:
      tags: [Batches]
      operationId: createBatch
      summary: Raise one signing per document for one set of parties
      description: |
        A batch is a **grouping, not a single signing act**. Every document
        keeps its own ceremony and its own signature, because that is what
        happens: the Lithuanian ID card produces one signature per document,
        and the external ceremony has no USB door where the product's batch
        signing lives. This buys one call instead of N and one identifier to
        poll. It does not buy one PIN for many documents, and nothing here
        should be read as promising that.

        Creation is atomic: either every signing in the batch is raised, or
        the error response means none was.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string, maxLength: 255 }
                documents:
                  type: array
                  minItems: 1
                  maxItems: 20
                  items:
                    type: object
                    properties:
                      name: { type: string }
                      content: { type: string, format: byte }
                      fileToken: { type: string, format: uuid }
                      flatten: { type: boolean }
                    required: [name]
                    additionalProperties: false
                signers:
                  type: array
                  minItems: 1
                  items: { $ref: '#/components/schemas/SignerRequest' }
                delivery: { type: string, enum: [link, email] }
                signingOrder: { type: string, enum: [parallel, sequential] }
                webhookUrl: { type: string, format: uri }
                externalId: { type: string, maxLength: 120 }
                expiresInDays: { type: integer, minimum: 1 }
                softDeadlineInDays:
                  type: integer
                  minimum: 1
                  description: Applied to every signing the batch raises.
              required: [title, documents, signers]
              additionalProperties: false
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '201':
          description: The batch and every signing it raised.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Batch' }
        '400': { $ref: '#/components/responses/Problem' }
        '402': { $ref: '#/components/responses/Problem' }

  /v1/batches/{batchId}:
    get:
      tags: [Batches]
      operationId: getBatch
      summary: Poll a batch and every signing in it
      parameters: [{ $ref: '#/components/parameters/BatchId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The batch.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Batch' }
        '404': { $ref: '#/components/responses/Problem' }

  /v1/signings/{signingId}/archive:
    post:
      tags: [Signings]
      operationId: archiveSigning
      summary: Raise the signed output to a long-term level
      description: |
        PDF goes PAdES-B-T to B-LT, ASiC-E goes XAdES-T to XAdES-LT: the
        certificate chain and fresh revocation evidence are embedded so the
        signature can still be validated once the signing certificate has
        expired.

        ADOC is refused by name. ADOC-V1.0 specifies XAdES-T and its
        validators expect that level, so "upgrading" one would be a
        conformance risk dressed as a favour.

        Already-archived output comes back unchanged with `upgraded: false`,
        so calling this twice costs a parse and not a second signature. The
        pre-archive version is kept - an upgrade adds a version, never
        replaces one.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The level now held, and whether this call raised it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  signingId: { type: string, format: uuid }
                  level: { type: string, example: PAdES-B-LT }
                  upgraded: { type: boolean }
                  document: { $ref: '#/components/schemas/SignedDocument' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
        '422': { $ref: '#/components/responses/Problem' }

  /v1/files:
    post:
      tags: [Files]
      operationId: uploadFile
      summary: Store a document once for reuse across signings
      description: |
        Returns a `fileToken` a later signing may reference instead of
        carrying base64 again. The same intake as a posted document, so a
        file that would be refused inline is refused here rather than at the
        signing that uses it.

        The bytes are **copied** into each signing that uses the token, never
        shared by reference: deleting a file, or letting it expire, can never
        reach backwards into a signing that already used it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                content: { type: string, format: byte }
              required: [name, content]
              additionalProperties: false
      responses:
        '429': { $ref: '#/components/responses/Problem' }
        '201':
          description: The stored file.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StoredFile' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }

  /v1/files/{fileToken}:
    get:
      tags: [Files]
      operationId: getFile
      summary: Inspect a stored file
      parameters: [{ $ref: '#/components/parameters/FileToken' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The stored file.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StoredFile' }
        '404': { $ref: '#/components/responses/Problem' }
    delete:
      tags: [Files]
      operationId: deleteFile
      summary: Purge a stored file
      description: |
        Drops the bytes. Signings that already referenced the token are
        untouched, because they hold their own copy.
      parameters: [{ $ref: '#/components/parameters/FileToken' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The file, now purged.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StoredFile' }
        '404': { $ref: '#/components/responses/Problem' }

  /v1/signings/{signingId}/comments:
    get:
      tags: [Signings]
      operationId: listComments
      summary: Read the notes on a signing
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The comment thread, oldest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  signingId: { type: string, format: uuid }
                  comments:
                    type: array
                    items: { $ref: '#/components/schemas/Comment' }
        '404': { $ref: '#/components/responses/Problem' }
    post:
      tags: [Signings]
      operationId: addComment
      summary: Add a note to a signing
      description: |
        Your own thread, for your own audit trail. Comments are **not shown
        to signers**: the ceremony's copy is platform-controlled, so a
        signer-visible comment would need a surface that does not exist, and
        one written in the belief that a signer will read it is worse than
        none.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                body: { type: string, maxLength: 4000 }
                authorLabel:
                  type: string
                  maxLength: 120
                  description: Your own name for whoever wrote it. Echoed back.
              required: [body]
              additionalProperties: false
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '201':
          description: The stored comment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Comment' }
        '400': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }

  /v1/erasure:
    post:
      tags: [Signings]
      operationId: eraseSignings
      summary: Erase terminal signings in bulk
      description: |
        Purges the client's terminal signing records and any order-owned
        uploads. Pre-existing vault documents used through MCP are preserved.
        In-flight signings are left untouched; erase one directly if
        cancellation is intended.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                before: { type: string, format: date-time }
              additionalProperties: false
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: Number purged and number that must be retried.
          content:
            application/json:
              schema:
                type: object
                properties:
                  purged: { type: integer }
                  failed: { type: integer }
                required: [purged, failed]
                additionalProperties: false
        '400': { $ref: '#/components/responses/Problem' }

  /v1/signings/{signingId}/cancel:
    post:
      tags: [Signings]
      operationId: cancelSigning
      summary: Cancel a signing
      description: |
        Retracts every invitation that has not been used: the ceremony link
        dies immediately, and the identity data the ceremony had captured
        about those signers is erased.

        Signatures already collected are untouched. A qualified signature
        exists once it is made, and this endpoint does not pretend otherwise
        - the signed document remains downloadable.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The signing is cancelled.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signing' }
        '404': { $ref: '#/components/responses/Problem' }
        '409':
          description: The signing had already ended.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }

  /v1/signings/{signingId}/document:
    get:
      tags: [Signings]
      operationId: downloadSignedDocument
      summary: Download the signed document
      description: |
        The newest signed output. Available as soon as one signer has
        signed, so a partially signed document can be fetched; the
        `X-Tvarka-Document-Sha256` header carries its digest. On a sandbox
        simulation this streams the unsigned artifact instead, marked by an
        `X-Tvarka-Sandbox` header.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The signed document.
          headers:
            X-Tvarka-Document-Sha256:
              schema: { type: string }
              description: SHA-256 of the returned bytes.
            X-Tvarka-Sandbox:
              schema: { type: string }
              description: |
                Present only on a sandbox artifact
                (`simulated-artifact-no-signature`). The bytes carry no
                signature.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404': { $ref: '#/components/responses/Problem' }
        '409':
          description: Nobody has signed yet.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }

  /v1/signings/{signingId}/signers:
    post:
      tags: [Signings]
      operationId: addSigner
      summary: Add a signer to a live signing
      description: |
        On a `sequential` signing the newcomer joins the END of the queue:
        they come back `waiting`, their link stays dead until their turn,
        and no delivery goes out before it. Only a parallel signing hands
        back a live ceremony URL immediately.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SignerRequest' }
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '201':
          description: The signer and their ceremony.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signer' }
        '400': { $ref: '#/components/responses/Problem' }
        '402': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }

  /v1/signings/{signingId}/signers/{signerId}:
    patch:
      tags: [Signings]
      operationId: updateSignerMethods
      summary: Update a pending signer's allowed methods
      parameters:
        - { $ref: '#/components/parameters/SigningId' }
        - { $ref: '#/components/parameters/SignerId' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                methods:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: { $ref: '#/components/schemas/SigningMethod' }
              required: [methods]
              additionalProperties: false
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The updated signer.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signer' }
        '400': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
    delete:
      tags: [Signings]
      operationId: removeSigner
      summary: Remove a pending signer
      description: Retracts the ceremony link before removing the signer.
      parameters:
        - { $ref: '#/components/parameters/SigningId' }
        - { $ref: '#/components/parameters/SignerId' }
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '204': { description: The signer was removed. }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }

  /v1/signings/{signingId}/signers/{signerId}/remind:
    post:
      tags: [Signings]
      operationId: remindSigner
      summary: Remind one signer
      description: Re-sends the invitation email. One reminder per signer per hour.
      parameters:
        - { $ref: '#/components/parameters/SigningId' }
        - { $ref: '#/components/parameters/SignerId' }
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '200':
          description: The reminder was sent.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signer' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
        '429':
          description: A reminder was sent to this signer less than an hour ago.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }

  /v1/signings/{signingId}/simulate:
    post:
      tags: [Signings]
      operationId: simulateSigning
      summary: Drive a sandbox signing to an outcome
      description: |
        Sandbox keys only. Moves a signing to `completed`, `declined` or
        `expired` so an integrator can exercise each terminal path without
        waiting. `complete` simulates the lifecycle: signer status is
        `simulated`, completion mode is `sandbox_simulation`, and no
        signature is created. It does return a downloadable artifact so the
        success path is testable, flagged `sandbox: true`. The sandbox adds
        no signature: an unsigned PDF comes back stamped and unsigned,
        anything already signed comes back byte-identical.
      parameters: [{ $ref: '#/components/parameters/SigningId' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                action:
                  type: string
                  enum: [complete, decline, expire]
                signerEmail:
                  type: string
                  format: email
                  description: Which signer declines. Defaults to the first.
                reason:
                  type: string
              required: [action]
              additionalProperties: false
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: The signing after the simulated outcome.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Signing' }
        '400': { $ref: '#/components/responses/Problem' }
        '403':
          description: The key is a production key.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409':
          description: |
            The signing is already terminal, so there is no outcome left to
            simulate.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/Problem' }

webhooks:
  signingEvent:
    post:
      summary: Signing event
      description: |
        Sent to `webhookUrl` as a signing progresses. Deliveries carry
        `X-Tvarka-Signature: sha256=<HMAC-SHA256 of the raw body>` keyed with
        your webhook secret - verify it before acting - plus
        `X-Tvarka-Idempotency-Key` and `X-Tvarka-Event`.

        Retried with backoff on 5xx and transport failures. Delivery is a
        convenience, never the only way to learn an outcome: polling always
        works, and a failed delivery never changes a signing's state.

        The payload never carries a signer's identity data - no personal
        code, no certificate subject, no phone number. It echoes the email
        you supplied and nothing more.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum:
                    - signing.signer_signed
                    - signing.signer_declined
                    - signing.completed
                    - signing.declined
                    - signing.cancelled
                    - signing.expired
                    - signing.failed
                signingId: { type: string, format: uuid }
                status: { $ref: '#/components/schemas/SigningStatus' }
                externalId: { type: string }
                occurredAt: { type: string, format: date-time }
                completionMode:
                  type: string
                  enum: [qes_ceremony, sandbox_simulation]
                signer:
                  type: object
                  properties:
                    signerId: { type: string, format: uuid }
                    email: { type: string, format: email }
                    status: { $ref: '#/components/schemas/SignerStatus' }
                    declineReason: { type: string }
                document:
                  $ref: '#/components/schemas/SignedDocument'
      responses:
        '401': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '200':
          description: Acknowledged. Any 2xx stops redelivery.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer <key>`. Keys are provisioned by Tvarka during
        onboarding and shown once. A `tsk_live_` key acts on production; a
        `tsk_test_` key is the sandbox.

        The key acts as a named workspace user. If that person's role is
        revoked, or the workspace is suspended, the key stops working.

  parameters:
    BatchId:
      name: batchId
      in: path
      required: true
      schema: { type: string, format: uuid }
    FileToken:
      name: fileToken
      in: path
      required: true
      schema: { type: string, format: uuid }
    SigningId:
      name: signingId
      in: path
      required: true
      schema: { type: string, format: uuid }
    SignerId:
      name: signerId
      in: path
      required: true
      schema: { type: string, format: uuid }

  responses:
    Problem:
      description: An RFC 9457 problem document.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }

  schemas:
    CreateSigningRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 255
          description: Shown to signers on the ceremony page and in their email.
        document:
          type: object
          properties:
            name:
              type: string
              description: |
                Filename with extension. The extension selects the family:
                `.pdf`; `.asice`/`.sce`/`.bdoc` to countersign an existing
                appendable ASiC-E; `.adoc`/`.bedoc`/`.cedoc`/`.gedoc`/
                `.ggedoc` to countersign a signed ADOC-family container.
                For containers the extension only decides that we look
                inside - the container's own mimetype member decides what
                it is. `.mdoc` is refused: an MDOC takes a first signature
                only.
            content:
              type: string
              format: byte
              description: |
                Base64-encoded bytes, up to 15 MiB decoded. Provide exactly
                one of `content` or `fileToken`.
            fileToken:
              type: string
              format: uuid
              description: |
                A reusable upload from `POST /v1/files`, referenced instead
                of carrying the bytes again. The bytes are copied into the
                signing at creation, so purging the file later never
                reaches into a signing that already used it. Provide
                exactly one of `content` or `fileToken`.
            flatten:
              type: boolean
              default: false
              description: |
                PDF only. Draw every form field's appearance into page
                content and drop the AcroForm, so what a reader sees cannot
                depend on a viewer's form handling. Fails closed: if any
                widget has no appearance stream Tvarka can draw faithfully,
                nothing is flattened and the response names those fields.
                Signing a live AcroForm without this is perfectly valid -
                the signature still detects any later edit.
          required: [name]
          additionalProperties: false
        container:
          type: object
          description: |
            Compose a NEW container from several files and sign that, instead
            of posting one finished document. Mutually exclusive with
            `document`; exactly one of the two is required.
          properties:
            format:
              type: string
              enum: [asice, adoc]
              description: |
                `asice` takes PDFs only. `adoc` is the Lithuanian ADOC-V1.0
                package and takes `.pdf` and `.xlsx`. The first file is the
                main document; the rest are appendices.
            files:
              type: array
              minItems: 1
              maxItems: 20
              items:
                type: object
                properties:
                  name: { type: string }
                  content: { type: string, format: byte }
                required: [name, content]
                additionalProperties: false
          required: [format, files]
          additionalProperties: false
        signers:
          type: array
          minItems: 1
          maxItems: 20
          description: |
            Everyone who must sign. Unordered, and one email may appear
            once - a person signs a document once.
          items: { $ref: '#/components/schemas/SignerRequest' }
        delivery:
          type: string
          enum: [link, email]
          default: link
          description: |
            `link` returns a `ceremonyUrl` per signer for you to deliver.
            `email` has Tvarka email each signer in your entity's name.
        webhookUrl:
          type: string
          format: uri
          description: Public HTTPS URL for signing events. Optional.
        externalId:
          type: string
          maxLength: 120
          description: Your own reference, echoed on every response and webhook.
        signingOrder:
          type: string
          enum: [parallel, sequential]
          default: parallel
        softDeadlineInDays:
          type: integer
          minimum: 1
          description: |
            Sets the soft deadline. On the day it passes, every signer who can
            still sign is reminded once. Independent of `expiresInDays`, which
            is the hard deadline.
        expiresInDays:
          type: integer
          minimum: 1
          maximum: 30
          default: 14
      required: [title, signers]
      oneOf:
        - required: [document]
        - required: [container]
      description: |
        Exactly one of `document` (a finished file) or `container` (several
        files composed into a new one) is required.
      additionalProperties: false

    SignerRequest:
      type: object
      properties:
        role:
          type: string
          enum: [signer, viewer]
          default: signer
          description: |
            A `viewer` is recorded but never invited, never gets a ceremony
            link, and never holds up completion. Tvarka sends them nothing.
            At least one signer is required.
        email: { type: string, format: email }
        name: { type: string, maxLength: 500 }
        language:
          type: string
          enum: [lt, en]
          default: lt
          description: Per signer, for their email and ceremony page.
        personalCode:
          type: string
          pattern: '^[0-9]{11}$'
          description: |
            Optional Lithuanian identity lock. Tvarka stores a keyed hash
            and encrypted value, never returns the code, and refuses a
            different authenticated signer.
        methods:
          type: array
          minItems: 1
          uniqueItems: true
          default: [nfc, smart_id, mobile_id]
          items: { $ref: '#/components/schemas/SigningMethod' }
          description: |
            Per-signer allow-list. `nfc` uses the Tvarka Sign mobile app;
            Smart-ID and Mobile-ID use the hosted page or mobile app.
      required: [email]
      additionalProperties: false

    SigningList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Signing' }
        hasMore: { type: boolean }
        nextCursor: { type: string, format: uuid, nullable: true }
      required: [data, hasMore, nextCursor]
      additionalProperties: false

    Signing:
      type: object
      properties:
        signingId: { type: string, format: uuid }
        status: { $ref: '#/components/schemas/SigningStatus' }
        title: { type: string }
        delivery: { type: string, enum: [link, email] }
        signingOrder:
          type: string
          enum: [parallel, sequential]
          description: |
            `sequential` releases one signer at a time in the order they were
            posted. The signers who are waiting hold a ceremony link that
            answers 404 until their turn, so the ordering is enforced rather
            than merely unsent. A decline stops the workflow at the
            decliner's turn: the signing settles `declined` and the signers
            still waiting are cancelled.
        softDeadlineAt:
          type: string
          format: date-time
          nullable: true
          description: |
            The date the signing is wanted by, as opposed to `expiresAt`,
            which is the date it dies on. Nothing expires here; it is when the
            one automatic reminder goes out.
        externalId: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        expiresAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
        completionMode:
          type: string
          enum: [qes_ceremony, sandbox_simulation]
          nullable: true
          description: |
            `qes_ceremony` means human eID signing. `sandbox_simulation`
            exercises lifecycle/webhook integration and creates no
            signature; its artifact is flagged `sandbox: true`.
        erasedAt: { type: string, format: date-time, nullable: true }
        detail:
          type: string
          description: Present when the status needs a sentence of explanation.
        signers:
          type: array
          items: { $ref: '#/components/schemas/Signer' }
        document: { $ref: '#/components/schemas/SignedDocument' }
      additionalProperties: false

    Signer:
      type: object
      properties:
        signerId: { type: string, format: uuid }
        role:
          type: string
          enum: [signer, viewer]
          description: |
            A `viewer` is a recorded non-signing party: no invitation is
            raised, no ceremony link exists, and they never hold up
            completion. Tvarka sends them nothing - you hold the webhook and
            the download, and distribute. At least one signer is required.
        email: { type: string, format: email, nullable: true }
        name: { type: string, nullable: true }
        language: { type: string, enum: [lt, en] }
        status: { $ref: '#/components/schemas/SignerStatus' }
        ceremonyUrl:
          type: string
          format: uri
          description: |
            Present only while this signer can still sign. A finished or
            retracted invitation has no live URL.
        signedAt: { type: string, format: date-time, nullable: true }
        declinedAt: { type: string, format: date-time, nullable: true }
        declineReason: { type: string }
        methodsAllowed:
          type: array
          items: { $ref: '#/components/schemas/SigningMethod' }
        actualMethod:
          allOf: [{ $ref: '#/components/schemas/SigningMethod' }]
          nullable: true
          description: Present after a real signature; contains no identity data.
        identityLocked: { type: boolean }
        identityMatched:
          type: boolean
          nullable: true
          description: True only after a locked signer completes the real ceremony.
        simulatedAt: { type: string, format: date-time, nullable: true }
      additionalProperties: false

    Batch:
      type: object
      properties:
        batchId: { type: string, format: uuid }
        title: { type: string }
        externalId: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        signings:
          type: array
          items: { $ref: '#/components/schemas/Signing' }

    StoredFile:
      type: object
      properties:
        fileToken: { type: string, format: uuid }
        name: { type: string }
        mediaType: { type: string }
        sha256: { type: string }
        bytes: { type: integer }
        createdAt: { type: string, format: date-time }
        expiresAt:
          type: string
          format: date-time
          description: |
            An unreferenced upload is not kept forever just because nobody
            deleted it. Re-upload after this, or post the bytes inline.
        usedCount:
          type: integer
          description: How many signings have copied these bytes.

    Comment:
      type: object
      properties:
        commentId: { type: string, format: uuid }
        author: { type: string, enum: [client, tvarka] }
        authorLabel: { type: string, nullable: true }
        body: { type: string }
        createdAt: { type: string, format: date-time }

    SignedDocument:
      type: object
      description: |
        The newest signed output. Absent until somebody signs, except on a
        sandbox simulation, where it describes the unsigned artifact.
      properties:
        sha256: { type: string }
        bytes: { type: integer }
        mimeType: { type: string }
        sandbox:
          type: boolean
          description: |
            Present and true only on a sandbox simulation. The bytes are a
            real file to which no signature was added.
        signed:
          type: boolean
          description: |
            Present and false only on a sandbox simulation. Absent on a real
            signing, where the output is signed by definition.

    SigningStatus:
      type: string
      enum: [pending, completed, declined, cancelled, expired, failed]
      description: |
        `pending` - at least one signer can still sign.
        `completed` - every signer signed.
        `declined` - somebody declined and nobody else can still sign.
        `expired` - the window closed first.
        `cancelled` - you retracted it.
        `failed` - the signing could not be prepared.

    SignerStatus:
      type: string
      enum:
        [pending, signed, declined, cancelled, expired, simulated,
         waiting, recorded]
      description: |
        `waiting` - a sequential signer whose turn has not come; their
        ceremony link answers 404 until it does.
        `recorded` - a viewer. Never invited, never blocks completion.

    SigningMethod:
      type: string
      enum: [nfc, smart_id, mobile_id]
      description: |
        The orchestration methods. For direct LT ATK reader, USB, raw CAdES
        and lower-level NFC integration, use the ATK API.

    ProblemFieldError:
      type: object
      description: One rejected field.
      properties:
        pointer:
          type: string
          description: |
            RFC 6901 JSON Pointer into the request body, for example
            `/signers/0/email` or `/container/files/2/content`.
        detail: { type: string }
      required: [pointer, detail]

    Problem:
      type: object
      description: RFC 9457.
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        errors:
          type: array
          description: |
            Present on `validation-failed`, and on the single-fault problems
            that name a field. Request-body validation collects rather than
            failing fast, so a caller who got three fields wrong learns all
            three from one response.
          items: { $ref: '#/components/schemas/ProblemFieldError' }
        recoveryUrl:
          type: string
          format: uri-reference
          description: |
            Present on the payment-required refusal when a web page exists
            that fixes it: the tenant's payment setup, reached with an
            ordinary browser login. Show it to your operator; it is not an
            API endpoint.
