Every API here is available over the APIs.io API and to AI agents over MCP.
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