Tvarka Sign API

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 on the hosted page or in the Tvarka Sign mobile app (LT ATK over NFC, Smart-ID, Mobile-ID). Nothing in the API signs anything - a signature is always a human act with the signer's own eID - so every operation is about getting a document in front of the right person and learning what they did. Orchestration sibling of the card-level ATK API; RFC 9457 problem details, HMAC-signed webhooks, and a hosted MCP server.

Operations 21

GET /v1/signings List signings #
POST /v1/signings Create a signing #
GET /v1/signings/{signingId} Get a signing #
PATCH /v1/signings/{signingId} Update a live signing deadline #
DELETE /v1/signings/{signingId} Erase one signing #
POST /v1/batches Raise one signing per document for one set of parties #
GET /v1/batches/{batchId} Poll a batch and every signing in it #
POST /v1/signings/{signingId}/archive Raise the signed output to a long-term level #
POST /v1/files Store a document once for reuse across signings #
GET /v1/files/{fileToken} Inspect a stored file #
DELETE /v1/files/{fileToken} Purge a stored file #
GET /v1/signings/{signingId}/comments Read the notes on a signing #
POST /v1/signings/{signingId}/comments Add a note to a signing #
POST /v1/erasure Erase terminal signings in bulk #
POST /v1/signings/{signingId}/cancel Cancel a signing #
GET /v1/signings/{signingId}/document Download the signed document #
POST /v1/signings/{signingId}/signers Add a signer to a live signing #
PATCH /v1/signings/{signingId}/signers/{signerId} Update a pending signer's allowed methods #
DELETE /v1/signings/{signingId}/signers/{signerId} Remove a pending signer #
POST /v1/signings/{signingId}/signers/{signerId}/remind Remind one signer #
POST /v1/signings/{signingId}/simulate Drive a sandbox signing to an outcome #

Documentation

Specifications

Other Resources

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/tvarka-sign-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no email required.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

tvarka-sign-api-openapi.yml Raw ↑
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:
  

# --- truncated at 32 KB (46 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/tvarka/refs/heads/main/openapi/tvarka-sign-api-openapi.yml