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=` 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 `. 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.