Base URLhttps://api.sigill.ai
AuthenticationAuthorization: Bearer <token>
Formatsapplication/json

JWT (from /auth/login) and API key (from Settings → API Keys) are both accepted as Bearer tokens. API keys are recommended for scripts and CI.

Errors use RFC 7807 application/problem+json, with status, title, and a human-readable detail. Resource-not-found responses may be an empty 404.

Authentication

POST/auth/registerpublic

Create a new user account and a new organisation tenant. The response carries no token: the account activates at first login, after the emailed verification link (valid 48 hours) is clicked. Person and organisation name are required; disposable email domains are rejected. Existing organisations are joined through an invitation or verified identity-provider flow, never by supplying a matching email domain at signup.

Request body
{
  "name": "string",
  "email": "string",
  "password": "string (min 10)",
  "organisationName": "string"
}
Response 200
{
  "message": "Account created. Check your inbox…",
  "email": "string"
}
POST/auth/loginpublic

Authenticate and receive a JWT.

Request body
{
  "email": "string",
  "password": "string"
}
Response 200
{
  "token": "<jwt>",
  "userId": "uuid",
  "email": "string",
  "tenantId": "uuid|null"
}
POST/auth/refreshJWT

Re-issue a JWT with the latest tenant context. Call after creating an organisation.

Response 200
{
  "token": "<new jwt>"
}
GET/auth/meJWT

Return the current user's profile decoded from the JWT.

Timestamping

Timestamp and verification endpoints accept digests and TSR evidence only. Deprecated full-file stamp/verify and raw TSA proxy endpoints have been removed.

POST/tsa/stamp-hashJWT / API key

Timestamp a document by its hash. Compute SHA-256 (or SHA-384 / SHA-512) of the file locally — only the digest is sent to Sigill. The file never leaves your machine. This is how RFC 3161 is meant to work.

Request body
{
  "hashHex": "hex digest — SHA-256 (64 chars), SHA-384 (96), or SHA-512 (128)",
  "tsaSlug": "qualified=false: auto | byot:<id-or-slug>; qualified=true: qualified option where available",
  "label": "string (optional)",
  "force": "boolean — re-stamp an already-stamped hash (default false)",
  "qualified": "boolean — request an eIDAS-qualified timestamp (default false)",
  "sealOperationId": "uuid (optional) — ties the stamp to one of your seal operations; used by the SDK's delegated PAdES flow to register the archival DocTimeStamp on the seal's evidence record",
  "reminders": "inherit | on | off (optional) — expiry-reminder policy for the created evidence; default inherit follows the organisation setting",
  "reminderDays": "30 | 60 | 90 | 180 (optional) — threshold override when reminders is on",
  "secondaryHashHex": "hex digest (optional) — the SAME content under a DIFFERENT supported algorithm (e.g. SHA-512 alongside a SHA-256 primary). Recorded as a CLIENT ASSERTION: Sigill never sees the content and no timestamp covers this digest, so it does not by itself carry evidentiary weight — it is captured while you still hold the original as input for future algorithm-migration protocols, and exported as such in the audit package. If the hash was already stamped: an empty secondary is filled in, an identical one is a no-op, a conflicting one returns 409",
  "tags": [
    "string[] (optional) — up to 10 distinct tags to attach to the evidence at creation; more than 10 in the request is a 400. When the evidence already carries tags, inserts stop silently at the 10-tag cap. With sealOperationId, tags attach to the SEAL's document hash — the evidence the list shows — not the timestamp imprint"
  ]
}
Response 200
{
  "serial": "string",
  "genTime": "ISO 8601",
  "hashAlgorithmOid": "OID string",
  "hashHex": "hex string",
  "tsrBase64": "base64 .tsr token",
  "tsaName": "string — timestamp token issuer metadata or customer authority label",
  "qualified": "boolean",
  "policyOid": "string | null — present when qualified: true"
}
Standard stamps use Sigill-managed automatic RFC 3161 timestamping when tsaSlug is "auto". Sigill may use multiple timestamp sources internally for reliability and failover; customers do not select individual Sigill-managed standard sources. Standard mode never routes to an eIDAS qualified option unless qualified: true is explicitly sent, and a standard response carrying a qualified policy is rejected rather than returned or stored. See Automatic TSA selection. If your organisation has already stamped this hash, the existing record is reused; force: true creates a second independent proof. Other organisations' records never affect yours — every organisation always gets its own evidence record.

You can also point tsaSlug at one of your verified tenant-private BYOT references from Timestamp Authorities when you need to use a customer-managed TSA endpoint.

Qualified stamps (qualified: true) route exclusively to eIDAS Qualified Trust Service Providers on the EU Trust List. Qualified stamps are counted separately: the Free plan receives three lifetime trial credits, while paid plans use their monthly qualified quota.
Response 502 — all TSAs failed (auto)
{
  "message": "All Sigill-managed timestamp sources failed.",
  "attemptsTried": 3,
  "failures": [
    {
      "source": "standard-pool-1",
      "errorClass": "timeout",
      "statusCode": null,
      "message": "Request timed out after 10s",
      "latencyMs": 10042
    }
  ]
}
Response 402 — quota exceeded
{
  "message": "Monthly stamp limit of 50 reached. Upgrade your plan to continue."
}
POST/tsa/restampJWT / API key

Archival renewal — never the original file. Profiled rfc4998-v1 chains renew natively per RFC 4998 §5.2: the new token covers the previous timeStamp content (for chains of three or more, the cover-all tree root over every predecessor), same digest algorithm throughout; a profiled chain whose links cannot be proven native fails with 409, never silently degraded. Legacy chains hash the previous full TimeStampResp (RFC 3161 §4 re-stamping). Qualification is inherited from the chain's origin by default: a qualified original renews via a qualified service (billed against your qualified quota), a standard original via the standard pool — so future renewals never downgrade the evidence. Links created before inheritance existed, or via an explicit override, may still be standard; each link's policy is visible in the chain. Pass qualified to override explicitly. A required qualified renewal that fails against the providers (network, auth, outage) returns 503 and is never silently downgraded. When every service answers validly but none extends the current horizon — typical for a fresh chain whose tip already carries the pool's longest cert — the response is instead 409 renewal_not_beneficial with the current horizon: the evidence is healthy, nothing is stored or billed, and no action is required (a standard downgrade is not suggested — it is not the answer to a healthy chain). Use before the renewal horizon passes.

Request body
{
  "transactionId": "uuid — the record to restamp",
  "tsaSlug": "auto | byot:<id-or-slug>",
  "qualified": "boolean (optional, tsaSlug=auto only — combined with a BYOT slug the request is a 400) — override the inherited qualification: true forces a qualified renewal, false explicitly accepts a standard one; omitted = inherit from the chain's origin"
}
Response 200
{
  "id": "uuid",
  "parentTransactionId": "uuid",
  "tsaName": "string",
  "genTime": "ISO 8601"
}
Use tsaSlug: "auto" for Sigill-managed standard timestamping. Use a verified BYOT timestamp authority when you need to restamp through a customer-managed TSA endpoint.
POST/tsa/verify-hashpublic

Verify a file against a TSR by its hash. The server validates the TSR token signature and timestamping certificate usage as well as the hash match. Call /tsa/inspect first to find the recorded hash algorithm, then compute that hash locally. The file never leaves your machine.

Request body
{
  "fileHashHex": "hex digest of the file (algorithm from /tsa/inspect)",
  "tsrBase64": "base64 .tsr token"
}
Response 200
{
  "valid": true,
  "message": "File matches the timestamp.",
  "details": {
    "serial": "string",
    "genTime": "ISO 8601",
    "hashAlgorithmOid": "string",
    "claimedHashHex": "string",
    "providedHashHex": "string",
    "hashMatch": true,
    "tokenSignatureValid": true
  }
}
POST/tsa/inspectpublic

Parse and return the contents of a TSR token without verifying against a file.

Request body
{
  "tsrBase64": "base64 .tsr token"
}
Response 200
{
  "genTime": "ISO 8601",
  "serial": "string",
  "hashAlg": "OID string",
  "tsaName": "string",
  "certNotBefore": "ISO 8601",
  "certNotAfter": "ISO 8601"
}

Automatic TSA selection

Pass tsaSlug: "auto" to /tsa/stamp-hash for Sigill-managed standard RFC 3161 timestamping; /tsa/restamp with auto additionally inherits the chain origin's qualification (see below). Seal operations with qualified: false via /seal/sign use the same path; passing qualified: true embeds a qualified RFC 3161 timestamp inside the seal — the seal itself remains an advanced electronic seal (AdES) regardless. Verified tenant-private BYOT authorities may be selected explicitly when you need to use a customer-managed endpoint.

How it works

  • Managed standard pool. Sigill may use multiple timestamp sources internally for reliability and failover. The exact internal source is not customer-selectable unless you configure BYOT.
  • Failover on the same request. If an internal source fails, Sigill can try another source before returning an error. The caller only sees the final outcome.
  • BYOT stays customer-managed. Verified tenant-private timestamp authorities are selected by their BYOT reference. If one returns a token carrying a qualified policy, standard mode rejects that response.
  • Every internal failure is logged. Errors are stored with classification (network, timeout, http_status, parse) for Sigill operations monitoring.

When to use auto

Default to auto in production. Use a verified BYOT timestamp authority when you need to use a customer-managed TSA endpoint.

Error response when all TSAs fail

If every Sigill-managed standard source fails within the same request, you get a 502 Bad Gateway with anonymized failure details:

{
  "message": "All Sigill-managed timestamp sources failed.",
  "attemptsTried": 3,
  "failures": [
    {
      "source": "standard-pool-1",
      "errorClass": "timeout",
      "statusCode": null,
      "message": "Request timed out after 10s",
      "latencyMs": 10042
    },
    {
      "source": "standard-pool-2",
      "errorClass": "http_status",
      "statusCode": 503,
      "message": "service unavailable",
      "latencyMs": 412
    },
    {
      "source": "standard-pool-3",
      "errorClass": "parse",
      "statusCode": 200,
      "message": "malformed TSR: unexpected ASN.1 tag",
      "latencyMs": 980
    }
  ]
}
The errorClass values are stable strings you can match in client code: network (connection refused, DNS, TLS), timeout (HTTP client timeout), http_status (TSA responded non-2xx), parse (response wasn't a valid TSR), unexpected_qualified (a standard candidate returned a qualified-policy token), unknown (catch-all).

Lookup

GET/api/lookup/{hash}public

Check if a file hash has been stamped. The hash must be a SHA-512 hex string. This endpoint is public — no authentication required.

Discoverability is opt-in. Records only appear here when the owning organisation allows public existence checks — either the organisation-wide setting (Settings → Public existence checks, default off) or a per-evidence override from the evidence detail page. Intended for publicly verifiable artifacts (software releases, published documents): anyone holding the file can confirm it was timestamped and is unmodified, by hash only. A hash whose owner has not opted in answers exactly like one that was never stamped. To look up your own records regardless of this setting, use the authenticated GET /api/transactions/by-hash/{hash}.

Path parameter
hash  SHA-512 hex string (128 chars)
Response 200
{
  "found": true,
  "count": 2,
  "latest": {
    "id": "uuid",
    "hash": "hex string",
    "alg": "OID string",
    "tsaName": "string",
    "genTime": "ISO 8601",
    "certNotAfter": "ISO 8601",
    "hasTsr": true
  },
  "records": [
    "same public cryptographic fields"
  ]
}
POST/api/transactions/{id}/public-lookupJWT · owner

Per-evidence override of the organisation's public-existence-check default, stored on the evidence (same tri-state contract as the reminder override). inherit follows the organisation setting, on makes this evidence publicly checkable even when the organisation default is off, off hides it even when the default is on. Owner only — the same role that controls the organisation default; members and API keys are rejected. Audited as evidence.public_lookup_changed.

Request body
{
  "mode": "inherit | on | off"
}
Response 200
{
  "mode": "on",
  "discoverable": true
}

Timestamps

GET/api/transactions/detailsJWT / API key

Paginated list of timestamp records for a tenant, with optional search. Each item includes hasRestamp so clients can identify records whose archival renewal chain has already advanced.

Query parameters
tenantId   uuid (required)
page       integer (default 1)
pageSize   integer (default 50, max 200)
search     string — filters on label or hash prefix (optional)
Response 200
{
  "total": 42,
  "page": 1,
  "pageSize": 50,
  "items": [
    {
      "id": "uuid",
      "parentTransactionId": null,
      "isRestamp": false,
      "hasRestamp": true,
      "certNotAfter": "ISO 8601"
    }
  ]
}
PATCH/api/transactions/{id}/labelJWT / API key

Set or update the label on a timestamp record.

Request body
{
  "label": "string (max 255)"
}
Response 200
{
  "id": "uuid",
  "label": "string"
}
GET/api/evidenceJWT / API key

The Evidence Store list, grouped by artifact: one row per content hash (a document, archive, payload — anything hashed). Restamp-chain members and seal-linked timestamp transactions fold into their evidence; multiple top-level records of the same artifact (re-seals, independent stamps) merge into one row with a record count. The row's horizon is the earliest member horizon — the date needing action first governs the artifact's status — and the primary record (the newest) is where the row links.

Query parameters
tenantId   uuid (required)
page       integer (default 1)
pageSize   integer (default 50, max 200)
search      string — label contains, tag contains, hash
            prefix, token serial (hex or decimal), TSA
            policy OID, or signer-cert SHA-256 (exact or
            ≥8-char prefix); token matches resolve to
            their artifact row
archived   boolean — list the archive view (default false)
formats     repeatable — rfc3161 | pades | cades | jades | pqc
statuses    repeatable — valid | expiring | expired
verify      repeatable — valid | failed | indeterminate
            | unverified (latest verification verdict)
tsas        repeatable — active TSA name
tags        repeatable — customer-defined tag
            (case-insensitive)
ers         boolean — only evidence with an RFC 4998-exportable stamp chain (rfc4998-v1 root, ers-native renewals)
needsAction boolean — only the work queue: expired,
            failed verification, or an unacknowledged
            expiry alarm
sort        lastEvent (default) | horizon (most urgent
            first) | lastVerified (stalest first)
hideAgentEvents boolean — drop quiet per-event seals of
            agent runs (signature-only, profile
            agent_event, nothing to act on); ignored
            with needsAction or a verify/statuses
            filter

Facet filters apply before pagination — OR within a facet, AND across facets. Unknown formats/statuses/verify/sort values return 400. expiring follows the evidence-reminder policy: the horizon is inside the effective reminder window (per-evidence override, else the tenant default) and reminders are enabled for that evidence; expired always applies regardless of policy. verify reflects the latest persisted verification run per chain — worst-of across an evidence's chains — where indeterminate means the material was insufficient (e.g. TSR not stored), never that verification failed.

Response 200
{
  "total": 5,
  "page": 1,
  "pageSize": 50,
  "stats": {
    "total": 5,
    "valid": 4,
    "expiring": 1,
    "expired": 0,
    "noHorizon": 0,
    "ersExportable": 2,
    "needsAction": 1,
    "verify": {
      "valid": 3,
      "failed": 1,
      "indeterminate": 0,
      "unverified": 1
    },
    "alarm": {
      "enabled": true,
      "days": 90
    },
    "byFormat": {
      "rfc3161": 2,
      "cades": 1
    },
    "byTsa": {
      "GlobalSign": 2
    },
    "byTag": {
      "q3-audit": 2
    },
    "horizonBuckets": [
      {
        "key": "expired | yyyy-MM (24 months) | later",
        "n": 0
      }
    ]
  },
  "tabs": {
    "needsAction": 1,
    "active": 5,
    "expiring": 1,
    "failedVerify": 1,
    "archived": 1
  },
  "sweep": {
    "lastSweepAt": "ISO 8601 | null",
    "running": false
  },
  "agentEvents": 0,
  "items": [
    {
      "hash": "hex",
      "alg": "OID string",
      "label": "string | null",
      "formats": [
        "rfc3161",
        "cades"
      ],
      "records": 2,
      "pqc": true,
      "ersExportable": true,
      "tokens": 3,
      "lastEventAt": "ISO 8601",
      "horizon": "ISO 8601 | null",
      "alarmEnabled": true,
      "alarmDays": 90,
      "tags": [
        "q3-audit"
      ],
      "verifyVerdict": "valid | failed | indeterminate | unverified",
      "lastVerifiedAt": "ISO 8601 | null",
      "needsAction": false,
      "primaryType": "stamp | seal",
      "primaryId": "uuid",
      "activeTsaName": "string | null",
      "docTs": false,
      "level": "cades-t | cades-x-l | pades-b-t | jades-b-b | jades-b-t | null",
      "signatureOnly": false,
      "profile": "agent_event | control_basis | evaluation | null",
      "certificateNotAfter": "ISO 8601 | null (signature-only seals: signing-certificate expiry, display only)",
      "verifiable": true,
      "members": [
        {
          "type": "stamp | seal",
          "id": "uuid",
          "at": "ISO 8601",
          "format": "string",
          "label": "string | null"
        }
      ]
    }
  ]
}

stats aggregates the whole view (all pages, unaffected by search or facet filters): status counts, the verification-verdict distribution, the work-queue size, the tenant alarm policy, per-format and per-TSA row counts, and the monthly renewal-horizon distribution. revocation reports TSA-signer revocation monitoring: signersRevoked (distinct timestamp-authority signer certificates protecting this tenant's evidence that are now revoked) and evidencesAffected — the dashboard lights up and each affected row carries signerRevoked: true when this is non-zero. The hourly revocation sweep detects a signer that has gone revoked, emails the tenant's owners once, and records evidence.revocation_detected in the custody log. alarmEnabled/alarmDays on each row are the effective reminder policy of the member governing the row's horizon. verifyVerdict/lastVerifiedAt come from the latest persisted verification run; docTs marks a document-timestamp chain on the primary record (the PAdES B-LTA shape). level is the signature level ACHIEVED at seal time (e.g. cades-t when revocation data could not be embedded); null on stamps and on seals created before level tracking — no level is asserted then. sweep reports the background verification sweep: when it last completed and whether one is running now.

POST/api/evidence/archiveJWT / API key

Archive or restore whole evidences by content hash (1–200 per call). Every top-level record of each artifact moves to (or out of) the archive view. Archiving mutes expiry reminders instantly; restoring returns the reminder override to inherit.

Request body
{
  "hashes": [
    "hex",
    "hex"
  ],
  "archive": true
}
Response 200
{
  "archived": true,
  "documents": 2,
  "records": 3
}
GET/api/transactions/{id}/chainJWT / API key

Resolve the full archival restamp chain containing a transaction (any member ID resolves to the same chain, root first). Each token includes metadata parsed from the stored TSR — serial, TSA policy OID, qualified flag, message-imprint algorithm — plus the evidence's expiry horizon (the active token's TSA-cert notAfter) and its reminder state. Backs the evidence detail page.

Response 200
{
  "rootId": "uuid",
  "activeId": "uuid",
  "label": "string",
  "hash": "hex string",
  "storeTsr": true,
  "horizon": "ISO 8601",
  "reminder": {
    "enabled": true,
    "days": 90,
    "notifyEmail": "string",
    "riskAcknowledgedAt": null
  },
  "tokens": [
    {
      "id": "uuid",
      "index": 1,
      "tsaName": "string",
      "genTime": "ISO 8601",
      "certNotAfter": "ISO 8601",
      "serial": "string",
      "policyOid": "OID string",
      "qualified": false,
      "hashAlgorithmOid": "OID string",
      "signatureValid": true
    }
  ]
}
GET/api/transactions/{id}/chain.zipJWT / API key

Download every stored TSR in the restamp chain as a zip, together with a chain.json manifest describing the parent → child linkage. 404 when no TSRs are stored for the chain.

GET/api/transactions/{id}/audit-package.zipJWT / API key

One download with everything an independent reviewer needs, verifiable offline without access to Sigill: every stored RFC 3161 token (tokens/*.tsr), the certificates embedded in each token (certificates/*.pem), the chain linkage manifest (chain.json), the latest persisted verification report (verification.json), the chain-of-custody event log (events.json), the stored detached signature where one exists (signature/), archived TSA revocation material — the TSA chain’s certificates and CRL/OCSP responses, captured at restamp time and refreshed by an hourly background sweep while the TSA still publishes them, so the package stays verifiable offline after the responders go dark; revocation/snapshots.json explicitly lists any signer whose material is not captured yet (revocation/, RFC 3161 §4) — the standards exports (evidence-record.ers — a conformant RFC 4998 EvidenceRecord, for natively-renewed chains — and validation-report.xml — the ETSI TS 119 102-2 V1.4.1 report with its detached CAdES signature validation-report.xml.p7s, when a current verification run with full provenance exists), a SHA-256 checksum manifest over every entry (manifest.json), and a human-readable SUMMARY.txt with step-by-step verification instructions — verifiable with open, standards-based tooling, without Sigill (OpenSSL covers the RFC 3161 tokens; full evidence-record validation uses an RFC 4998 tool). The export itself is recorded in the evidence's custody log as evidence.exported.

GET/api/transactions/{id}/evidence-record.ersJWT / API key

The whole restamp chain as one conformant RFC 4998 EvidenceRecord (DER) — independent ERS tooling validates it without any Sigill documentation: the first ArchiveTimeStamp covers the artifact digest, each renewal covers the previous timeStamp content with the chain's single digest algorithm (§5.2/§5.3), and every token signature verifies against its embedded certificates. Available for chains whose root carries the rfc4998-v1 profile (set at creation for new plain-artifact evidence) and whose every renewal is provably native. Legacy/unprofiled roots — including all pre-existing evidence and seal-linked stamps — can never export as .ers; their export is the audit package. Chains with legacy renewal links are refused with 409: re-encoding cannot make them conformant, and their fully verifiable export remains the audit package's open, documented recipe. Also 409 on a broken/truncated chain or a link without a stored TSR — the builder verifies linkage cryptographically, never from metadata.

GET/api/transactions/{id}/validation-report.xmlJWT / API key

The evidence's latest persisted verification run as an ETSI TS 119 102-2 V1.4.1 validation report — the real schema (namespace http://uri.etsi.org/19102/v1.4.1#, validated against the ETSI XSD in tests and CI, plus an independent semantic checker). EN 319 102-1 three-state vocabulary (total-passed / total-failed / indeterminate — insufficient material is never reported as failed) with per-token sub-indications, the validation policy identified by URI and SHA-256 over its published, immutable text (/api/policies/chain-validation-v1), the root token's genTime as best-signature-time, and per-timestamp validation objects: signature, RFC 4998 cover-all proof-of-existence provisioning, trust anchoring, signer certificate, and the revocation status proven from capture-time snapshot material — citing the exact revocation/… file in the audit package that was consulted. total-passed requires proven-good revocation on every token; unprovable status degrades to indeterminate, never up. Reports are bound to one chain version: after a restamp, re-run verify-chain for a current report (earlier runs are kept as history). 404 until a run exists; 409 when the run predates the current chain or provenance capture.

GET/api/transactions/{id}/validation-report.zipJWT / API key

The issued report as a signed bundle. Reports are issued once per verification run — the exact XML bytes and their SHA-256 are stored and served byte-identically on every later download, so history is reproducible and KMS/TSA cost is per issuance, never per download. The bundle carries the XML, a detached CAdES signature by Sigill's platform certificate over those exact bytes with an RFC 3161 timestamp (validation-report.xml.p7s), the signer certificate, the exact validation-policy bytes (policies/chain-validation-v1.txt — the report's SigPolicyHash binds to them, so the rules the verdict was produced under are reconstructible offline; the public URI is a convenience, not the only source), and step-by-step OpenSSL verification instructions. Signed always means signed and timestamped: if the signature cannot be produced, the endpoint returns 503 instead of a silently unsigned bundle — the raw XML stays available at validation-report.xml. The signature authenticates that Sigill produced the report unaltered — deliberately separate from the evidence's own trust: the report cites external anchors (EU LOTL, TSA chains) for the evidence itself.

GET/api/transactions/{id}/validation-reportsJWT / API key

The issued-report history for the evidence: every validation report ever issued — current and superseded — with sha256, signing state, issue time and the supersession links. Reports are immutable snapshots; when one must be replaced (e.g. a remediation), the old row is preserved byte-for-byte and linked to its replacement — exactly one report per verification run is current. Fetch any specific report's exact bytes with validation-report.xml?reportId=<id> (superseded reports answer with an X-Report-Superseded-By header); the report must belong to this evidence's chain — a report id from another evidence is 404 even within the same tenant.

POST/api/transactions/{id}/verify-chainJWT / API key

Cryptographically verify the whole chain in one call: each token's TSA signature (RFC 3161 §2.2 with the timestamping EKU of §2.3), the hash linkage — the root token covers the recorded file digest; each renewal per its linkage protocol: ers-native covers previous timeStamp content (cover-all per RFC 4998 §5.2), legacy covers a digest of the previous full TimeStampResp (RFC 3161 §4) — and the signer certificate's chain trust, including the EU LOTL check for qualified tokens. Trust states: qualified_lotl, trusted_chain (anchor matched — EU LOTL, Sigill's pinned pool-TSA root anchors, or the OS trust store), valid_untrusted_chain, self_signed. Two independent top-level verdicts: cryptographicallyValid (signatures + linkage) and trustAnchored (every token anchored); valid requires both. verdict is the three-state summary in the spirit of ETSI EN 319 102-1: failed only on a definitive negative (broken signature or linkage); insufficient material — a TSR not stored, a signer chain that doesn't reach an anchor — is indeterminate, never collapsed into failed. Every run is persisted and becomes the evidence's last verified state in the Evidence Store.

Response 200
{
  "valid": true,
  "verdict": "valid | failed | indeterminate",
  "cryptographicallyValid": true,
  "trustAnchored": true,
  "checkedAt": "ISO 8601",
  "tokens": [
    {
      "id": "uuid",
      "index": 1,
      "signatureValid": true,
      "linkValid": true,
      "trust": "trusted_chain",
      "chainPath": [
        "subject DN chain"
      ]
    }
  ]
}
POST/api/transactions/{id}/revalidateJWT / API key

Queue a validation maturation for the evidence (returns 202). A freshly sealed artifact is cryptographically valid immediately, but the definitive ETSI verdict needs TSA revocation material issued after the newest token — until it is captured, the validation report is interim (indeterminate / TRY_LATER). The maturation fetches fresh revocation data only for the signer(s) whose material cannot yet prove their token (a per-signer advisory lock and cooldown keep a burst of seals off the TSA), then re-verifies the chain and regenerates the current validation report. Sigill queues the same maturation automatically about two minutes after every seal; this endpoint is the manual trigger. Poll GET /api/transactions/{id}/chain until validation.current.revocationStatus is proven (values: proven | pending | invalidated | not_assessed — invalidated is a definitive revocation failure, not a pending state). validation.current.bestEffort is true when the protective TSA is non-qualified — such authorities publish revocation slowly (a periodic CRL, often no per-certificate OCSP), so pending is expected and clears on the TSA's next CRL rather than indicating a fault; qualified TSAs prove promptly.

Response 202
{
  "queued": true
}
POST/api/evidence/tagsJWT / API key

Tag or untag whole evidences by content hash (1–200 per call) — the grouping and filtering dimension of the Evidence Store, distinct from the single display label. Tags attach to the artifact identity (content hash), so they survive re-seals and restamps. At most 10 tags per evidence and 40 characters per tag; case-preserving, case-insensitively unique and matched. Unknown hashes are skipped; evidences already at the cap are reported in capReached.

Request body
{
  "hashes": [
    "hex",
    "hex"
  ],
  "add": [
    "q3-audit"
  ],
  "remove": [
    "old-tag"
  ]
}
Response 200
{
  "evidences": 2,
  "added": 2,
  "removed": 1,
  "capReached": []
}
POST/api/evidence/verifyJWT / API key

Verify whole evidences by content hash (1–50 per call). Every chain of each artifact is verified — stamp chains and all chains of its seal operations (signature timestamp and DocTimeStamp) — with the same treatment as verify-chain, and every verdict is persisted. Returns the worst-of verdict per artifact.

Request body
{
  "hashes": [
    "hex",
    "hex"
  ]
}
Response 200
{
  "chains": 3,
  "items": [
    {
      "hash": "hex",
      "verdict": "valid | failed | indeterminate"
    }
  ]
}
POST/api/evidence/restampJWT / API key

Bulk archival renewal by content hash (1–200 per call): every member chain of each artifact — stamp chains and a seal’s signature-timestamp and DocTimeStamp chains — whose tip is inside its effective expiry-alarm window (or already expired) is queued for an RFC 3161 §4.3 restamp. The job runs on a durable, database-backed queue: it survives restarts and deployments, retries TSA throttling per item with growing cooldowns, and each restamp consumes one plain-timestamp quota token (on exhaustion, remaining items are skipped, never failed). Healthy tips and tips without a stored TSR are reported as skipped. dryRun: true returns the counts without queuing anything.

Request body
{
  "hashes": [
    "hex",
    "hex"
  ],
  "dryRun": false
}
Response 202 (job queued)
{
  "jobId": "uuid",
  "queued": 12,
  "skippedHealthy": 3,
  "skippedNoTsr": 1,
  "skippedQueued": 0
}
Response 200 (nothing to queue, or dryRun)
{
  "jobId": null,
  "queued": 0,
  "skippedHealthy": 3,
  "skippedNoTsr": 1,
  "skippedQueued": 2
}
skippedQueued counts chains that already have a renewal in flight — at most one live renewal per chain is enforced by the database, so overlapping submissions can never fork a chain. With dryRun: true the 200 body uses wouldQueue in place of queued and nothing is created.
POST/api/evidence/resealJWT / API key

Bulk re-seal by content hash (1–200 per call): one fresh detached seal per artifact and format — CAdES .p7s or JAdES .jades.json — produced from the newest eligible seal operation of each (artifact, format) group. Fidelity is preserved: hybrid ML-DSA where the chosen operation was post-quantum, the original JAdES content type, the original expiry-reminder policy, and — crucially — a qualified timestamp where the original was qualified (evidence is never downgraded; qualified re-seals consume qualified quota). A re-seal is only counted done when a fresh timestamp was embedded — TSA outages retry, they never deliver an untimestamped renewal. PAdES operations are reported as unsupported (use the SDK’s delegated flow); operations whose certificate is no longer active are skipped unless fallbackCertificateId names an active replacement, which is used only for those. Requires the tenant’s Store detached seals setting: artifacts are delivered from storage via GET /api/evidence/jobs/{id}/artifacts.zip. Runs on the same durable queue as bulk restamp; at most one live re-seal per operation is enforced by the database. dryRun: true returns counts only.

Request body
{
  "hashes": [
    "hex",
    "hex"
  ],
  "dryRun": false,
  "fallbackCertificateId": "uuid (opt) — active cert used only where the original is inactive"
}
Response 202 / 200
{
  "jobId": "uuid | null",
  "queued": 4,
  "skippedUnsupported": 1,
  "skippedCertInactive": 0,
  "skippedQueued": 0,
  "skippedNoSeals": 2
}
GET/api/evidence/jobs/{id}/artifacts.zipJWT / API key

The detached seals a re-seal job produced, served from stored artifacts, with a SHA-256 checksum manifest.json that also confesses its gaps: expected, delivered, and a missing list of operation ids whose artifact is not in storage. 404 while the job is still running or when nothing was produced; only re-seal jobs produce artifacts.

GET/api/evidence/jobs/{id}JWT / API key

Progress of a renewal job — poll it live or come back later; the job record is durable. status is running, completed, or completed_with_errors; problems lists the latest failed/skipped items with their reasons. GET /api/evidence/jobs returns the five most recent jobs.

Response 200
{
  "id": "uuid",
  "kind": "restamp",
  "status": "running | completed | completed_with_errors",
  "total": 12,
  "done": 9,
  "failed": 1,
  "skipped": 0,
  "createdAt": "ISO 8601",
  "completedAt": "ISO 8601 | null",
  "problems": [
    {
      "hash": "hex",
      "status": "failed | skipped",
      "error": "string"
    }
  ]
}
POST/api/evidence/verify-sweepJWT / API key

Re-verify every active chain of the organisation in the background — the "Verify all now" action. Each chain gets the same treatment as verify-chain and each verdict is persisted. Sigill also runs this sweep automatically once a day. At most one sweep per organisation runs at a time.

Response 202
{
  "started": true,
  "alreadyRunning": false
}
GET/api/transactions/{id}/eventsJWT / API key

Chronological event log for the evidence: stamping, archival restamps, sealing, reminder-setting changes, reminder emails, and risk acknowledgements — with the acting user where one exists.

Response 200
{
  "events": [
    {
      "at": "ISO 8601",
      "type": "stamped | restamped | timestamped | sealed | evidence.reminders_changed | evidence.risk_acknowledged | evidence.reminder_sent",
      "actor": "string | null",
      "detail": "string | null",
      "transactionId": "uuid | null"
    }
  ]
}
POST/api/transactions/{id}/remindersJWT / API key

Set the evidence's reminder override. The policy lives on the organisation (Settings → Evidence expiry reminders); evidences inherit it by default. mode: "off" mutes an evidence that is no longer relevant; "on" forces reminders even when the organisation default is off. Reminders arrive as one digest email per organisation, not one per evidence. A restamp moves the horizon forward and re-arms the reminder.

Request body
{
  "mode": "inherit | on | off",
  "days": "30 | 60 | 90 | 180 (optional, with mode on)"
}
Response 200
{
  "mode": "on",
  "enabled": true,
  "days": 90,
  "nextReminderAt": "ISO 8601"
}
POST/api/transactions/{id}/acknowledge-riskJWT

Record that a signed-in user accepts the current expiry risk. The acknowledgement is written to the tenant audit log and silences reminders until the next threshold crossing. Not available to API keys — acknowledgement is a personal act.

Response 200
{
  "riskAcknowledgedAt": "ISO 8601",
  "riskAcknowledgedBy": "string"
}

Timestamp Authorities

GET/proxy/servicesJWT / API key

List customer-selectable timestamping options available to the caller's tenant. Sigill-managed standard RFC 3161 timestamping is selected with tsaSlug: "auto" and internal standard sources are not returned here. Verified tenant-private BYOT authorities are returned as source: "tenant" entries; qualified timestamping options may appear as source: "qualified" when entitled.

Response 200
[
  {
    "id": "uuid",
    "name": "My timestamp authority",
    "proxySlug": "byot:my-tsa",
    "preferredHashOid": "2.16.840.1.101.3.4.2.3",
    "isQualified": false,
    "source": "tenant"
  },
  {
    "id": "uuid",
    "name": "Qualified timestamping",
    "proxySlug": "qualified",
    "preferredHashOid": "2.16.840.1.101.3.4.2.3",
    "isQualified": true,
    "source": "qualified"
  }
]
The underlying Sigill-managed standard timestamping pool is internal infrastructure. Customers do not select individual non-qualified Sigill-managed timestamp sources.

Timestamp Authorities

Tenants on the Business plan or above can add their own RFC 3161 endpoints under Settings → Timestamp authorities. Sigill verifies each configuration server-side before saving it, protects stored credentials with Data Protection, and exposes verified entries as tenant-private BYOT references.

GET/api/tenant/tsaJWT

List the authenticated tenant's BYO TSA configurations. Returns the verification status and the last successful test result for each row.

Response 200
{
  "planAllows": true,
  "plan": "business",
  "maxPerTenant": 10,
  "items": [
    {
      "id": "uuid",
      "label": "My TSA",
      "slug": "my-tsa",
      "endpointUrl": "https://tsa.example.com",
      "preferredHashOid": "2.16.840.1.101.3.4.2.3",
      "authType": "basic",
      "username": "tsa-user",
      "hasSecret": true,
      "isActive": true,
      "verifiedAt": "ISO 8601",
      "lastVerifySerialHex": "string",
      "lastVerifyTsaName": "string"
    }
  ]
}
POST/api/tenant/tsa/verifyJWT

Run a one-shot RFC 3161 probe against a candidate endpoint without saving it. This is the same server-side verification step used by Save.

Request body
{
  "endpointUrl": "https://tsa.example.com",
  "preferredHashOid": "2.16.840.1.101.3.4.2.3",
  "authType": "none | basic | bearer",
  "username": "string (basic only)",
  "secret": "string (password or bearer token)"
}
Response 200
{
  "ok": true,
  "serial": "string",
  "genTime": "ISO 8601",
  "hashAlgorithmOid": "OID string",
  "tsaName": "string",
  "lotlQualified": false
}
POST/api/tenant/tsaJWT

Create a BYO TSA after the server verifies it. Tenants below the Business plan get a 402. The configuration is saved only after the endpoint passes the live RFC 3161 check.

Request body
{
  "label": "My timestamp authority",
  "slug": "my-tsa",
  "endpointUrl": "https://tsa.example.com",
  "preferredHashOid": "2.16.840.1.101.3.4.2.3",
  "authType": "none | basic | bearer",
  "username": "string (basic only)",
  "secret": "string (password or bearer token)",
  "responsibilityConfirmed": true
}
PATCH/api/tenant/tsa/:idJWT

Update a BYO TSA. Transport changes trigger a fresh verification before the row is committed.

DELETE/api/tenant/tsa/:idJWT

Remove a BYO TSA. This remains available even after a plan downgrade so tenants can clean up their configuration.

Verified tenant timestamp authorities can be selected explicitly as tsaSlug values using their slug or byot:<slug>. BYOT endpoints are customer-managed; Sigill verifies technical RFC 3161 compatibility, not the customer's right to use the endpoint.

Seals

Cryptographic seals backed by KMS — the private key never leaves. An RFC 3161 timestamp is embedded in every seal automatically. Owner role required to manage certificates; any authenticated user can seal.

Every format has a hash-only path — the document itself never has to reach Sigill:

  • POST /seal/sign-pades-hash — the recommended way to seal PDFs. The SDK assembles the PDF signature revision locally and sends only the ByteRange SHA-256 digest; Sigill returns the PAdES CMS (ETSI EN 319 142-1) and the SDK embeds it. The PDF never leaves your environment. Supported out of the box by the .NET and Python SDKs (SealPadesAsync / seal_pades).
  • POST /seal/sign-hash — accepts a pre-computed SHA-256 digest only. Non-PDF files (e.g. JSONL, XML, plain text) receive a detached CAdES .p7s without the original content ever leaving the client. Same privacy model as /tsa/stamp-hash.
  • format: "jades" — on /seal/sign and /seal/sign-hash, returns a detached JAdES signature (ETSI TS 119 182-1, .jades.json) instead of CAdES. The ETSI signature format for JSON — recommended for JSON/JSONL data such as API payloads and AI conversation logs. Same detached, hash-only model as CAdES.
  • POST /seal/sign-hashes — multi-object JAdES: one seal binding an envelope plus any number of payloads, each by digest (sigD ObjectIdByURIHash). Built for AI-evidence records; neither the envelope nor content is ever transmitted. Verify object-level with POST /seal/verify-objects.
  • POST /seal/sign — uploads the full file; PDFs come back as a sealed PDF, other files as detached CAdES/JAdES. The convenience path used by this web app — prefer the hash-only endpoints for integrations. (The MCP server never uses it: MCP has no document upload or download capability.)
GET/seal/certificatesJWT / API key

List active signing certificates for the tenant. The Sigill platform certificate is always appended at the end with source: "platform".

Response 200
[
  {
    "id": "uuid",
    "label": "string",
    "status": "active | provisioning | revoked",
    "source": "byoc | platform",
    "certSubject": "CN=...",
    "certNotBefore": "ISO 8601",
    "certNotAfter": "ISO 8601"
  }
]
POST/seal/certificatesJWT (owner)

Provision a new signing key. Generates RSA-4096 in KMS and returns a PKCS#10 CSR for CA submission. Submit the CSR to a CA, then activate via the endpoint below. Seal signatures with this key use RSASSA-PSS by default (the ETSI TS 119 312-recommended RSA padding; SHA-256, MGF1-SHA-256; an operator-level rollback to PKCS#1 v1.5 exists) — existing PKCS#1 v1.5 seals remain valid and verifiable.

Request body
{
  "commonName": "Acme Corp Seal 2026",
  "organization": "Acme Corp AS",
  "countryCode": "NO",
  "organizationalUnit": "string (opt)",
  "locality": "string (opt)",
  "state": "string (opt)",
  "label": "string (opt)"
}
Response 200
{
  "id": "uuid",
  "label": "string",
  "status": "provisioning",
  "kmsKeyArn": "arn:aws:kms:...",
  "csrPem": "-----BEGIN CERTIFICATE REQUEST-----..."
}
POST/seal/certificates/:id/activateJWT (owner)

Upload the CA-signed certificate chain PEM. Validates that the public key matches the provisioned KMS key before activating.

Request body
{
  "certificatePem": "PEM certificate chain: leaf certificate plus optional intermediates"
}
Response 200
{
  "id": "uuid",
  "status": "active",
  "certSubject": "CN=...",
  "certNotBefore": "ISO 8601",
  "certNotAfter": "ISO 8601"
}
422 Unprocessable if the certificate's public key does not match the KMS key, or if the certificate is already expired.
DELETE/seal/certificates/:idJWT (owner)

Revoke a certificate and schedule the KMS key for deletion (7-day grace period). Cannot be undone. Platform certificate cannot be revoked via this endpoint.

Response 200
{
  "message": "Certificate revoked. KMS key deletion scheduled."
}
POST/seal/signJWT / API key

Seal a file by uploading it. Accepts multipart/form-data. File type is detected server-side from magic bytes — PDFs produce a PAdES-signed PDF (application/pdf); other files produce a detached CAdES .p7s (application/pkcs7-signature). Pass format: "jades" to get a detached JAdES .jades.json (application/jose+json) instead. For detached formats, pqc: true adds an ML-DSA-87 signer (a second CMS SignerInfo, or a second JWS signatures[] entry per RFC 9964). For non-PDF files where privacy matters, prefer POST /seal/sign-hash instead — it never receives the file content.

Form fields
{
  "file": "file to seal (required)",
  "certificateId": "uuid (required)",
  "label": "string — stored in operation log (opt)",
  "reason": "PDF only — written into PDF /Reason field (opt)",
  "location": "PDF only — written into PDF /Location field (opt)",
  "qualified": "boolean — embed a qualified RFC 3161 timestamp (default false)",
  "format": "\"jades\" for a detached JAdES signature (opt; default CAdES for non-PDF)",
  "pqc": "boolean — detached formats only, add ML-DSA-87 signer (default false)",
  "tags": "repeatable field (opt) — up to 10 distinct tags attached at creation; >10 in the request is a 400, inserts stop silently at an evidence's existing 10-tag cap"
}
Response headers
{
  "X-Seal-Operation-Id": "uuid",
  "X-Seal-Certificate-Id": "uuid",
  "X-Seal-Timestamped-By": "TSA name | none",
  "X-Seal-Format": "pades-b-lta | pades-b-lt | pades-b-t | pades-bes | cades-x-l | cades-t | cades-bes | jades-b-t | jades-b-b",
  "X-Seal-Qualified": "true | false",
  "X-Seal-Pqc": "ml-dsa-87 | none"
}
PDF response — sealed PDF (application/pdf), save as filename_sealed.pdf.

Non-PDF response — detached CAdES signature (application/pkcs7-signature), save as filename.p7s alongside the original file.

JAdES response (format: "jades") — detached JAdES signature (application/jose+json), save as filename.jades.json alongside the original file. The seal covers the exact bytes — re-serializing the original JSON breaks it by design.

If every Sigill-managed TSA fails, the seal is still produced but X-Seal-Timestamped-By is none and the format degrades to *-bes.

With Store PAdES seal data enabled in Settings, the CMS embedded in a sealed PDF is also escrowed and re-downloadable via GET /seal/operations/:id/p7s — repair material if the PDF's signature contents are ever damaged, and a detached proof object. It cannot reconstruct the sealed PDF on its own; keep the returned file. Off by default.
POST/seal/sign-hashJWT / API key

Seal a file by sending only its digest. The original file content never leaves the client — same privacy model as /tsa/stamp-hash. Returns a detached CAdES .p7s by default, or a detached JAdES .jades.json with format: "jades" — both verify against the original file. For pqc: true, send both SHA-256 (hashHex) for the classical signer and SHA-512 (hashHex512) for the ML-DSA signer.

Request body (JSON)
{
  "hashHex": "64-char lowercase SHA-256 hex of the file (required)",
  "certificateId": "uuid (required)",
  "label": "string — stored in operation log (opt)",
  "qualified": "boolean — embed a qualified RFC 3161 timestamp (default false)",
  "format": "\"cades\" (default) | \"jades\" (opt)",
  "contentType": "MIME type of the sealed object, e.g. application/json — JAdES only (opt)",
  "pqc": "boolean — add ML-DSA-87 signer (default false)",
  "hashHex512": "128-char SHA-512 hex of same file; required when pqc=true",
  "tags": [
    "string[] (opt) — up to 10 distinct tags attached at creation; >10 in the request is a 400, inserts stop silently at an evidence's existing 10-tag cap"
  ]
}
Response headers
{
  "X-Seal-Operation-Id": "uuid",
  "X-Seal-Certificate-Id": "uuid",
  "X-Seal-Timestamped-By": "TSA name | none",
  "X-Seal-Format": "cades-x-l | cades-t | cades-bes | jades-b-t | jades-b-b",
  "X-Seal-Qualified": "true | false",
  "X-Seal-Pqc": "ml-dsa-87 | none"
}
Response body is a detached CAdES .p7s (application/pkcs7-signature) or, with format: "jades", a detached JAdES signature (application/jose+json). Save it alongside the original file. To verify, supply both the original file and the detached signature to POST /seal/verify — the verifier recomputes the hash and checks it matches the digest embedded in the signature.

400 if hashHex is not a valid 64-character hex string. For PDFs, use /seal/sign-pades-hash (hash-only, recommended) or /seal/sign (upload).
POST/seal/sign-hashesJWT / API key

Seal a multi-object record — an envelope plus any number of content payloads — by digests alone (blind JAdES). Built for AI-evidence envelopes and sibling envelope formats: the envelope's canonical digest becomes signed object 0 (urn:sigill:envelope), each payload is bound by its own digest, and neither the envelope nor any content is ever transmitted or stored. Sigill signs the multi-object sigD with your seal certificate, timestamps it (unless timestamp: false), and returns the JAdES JWS — you assemble the final artifact locally.

Request body (JSON)
{
  "envelopeHashHex": "64-char SHA-256 hex of the canonical (RFC 8785) envelope (required)",
  "certificateId": "uuid (required)",
  "objects": [
    {
      "uri": "unique opaque URI, e.g. urn:uuid:… — use identifiers, not filenames (max 128 objects)",
      "hashHex": "64-char SHA-256 hex of the raw payload bytes",
      "hashHex512": "128-char SHA-512 hex; required when pqc=true",
      "contentType": "MIME type (opt)"
    }
  ],
  "envelopeContentType": "cty for object 0 (default application/vnd.sigill.ai-evidence+json)",
  "envelopeHashHex512": "128-char SHA-512 hex; required when pqc=true",
  "label": "string (opt)",
  "qualified": "boolean (default false)",
  "pqc": "boolean — add ML-DSA-87 hybrid signer (default false)",
  "tags": [
    "string[] (opt)"
  ],
  "timestamp": "boolean (default true) — false signs without a TSA timestamp (jades-b-b by request): for chained records where a later checkpoint's timestamp anchors the chain"
}
Response 200 (JSON)
{
  "signature": "General JWS JSON object — the JAdES signature; wrap it with your envelope as {envelope, signature}",
  "operationId": "uuid",
  "format": "jades-b-t | jades-b-b",
  "timestampedBy": "TSA name | null",
  "qualified": "boolean",
  "pqc": "boolean",
  "timestampRequested": "boolean — false when you asked for no timestamp; true with format jades-b-b means a requested timestamp was unavailable"
}
URIs are compared byte-exactly and must be unique; urn:sigill:envelope is reserved for the envelope itself. Keep URIs opaque — a filename in a URI leaks through any amount of hashing. Sigill persists only the seal operation and the signed digest list (hashes, content types, count) — never the URIs, never the envelope, never content. The operation's recorded document hash is the envelope digest, so grouping, tags, renewal reminders and archival restamps work like any other seal.

To verify, use POST /seal/verify-objects (digests only) or any TS 119 182-1 validator with the payload bytes. For a whole controlled AI-agent run (Agent Evidence Profiles v1, as the SDKs record it: a control basis, a chain of events, optional control evaluations), the Verify a run bundle page reads the envelopes in your browser — the control basis and its binding, sequence, chain links, the signed timestamp policy, finalization, evaluations — and asks Sigill only the blind question, one POST /seal/verify-objects per artifact with the signature and locally computed digests. No envelope and no content leaves your machine; an SDK does the same offline.

400 on duplicate or reserved URIs, malformed digests, content types that are not plain MIME types (type/subtype — they are persisted, so free text is rejected), or more than 128 objects; 404 if the certificate is unknown, inactive, or not visible to the tenant.
POST/seal/sign-pades-hashJWT / API key

Delegated PAdES signing — seal a PDF without uploading it. The client prepares the PDF signature revision locally (placeholder /Contents slot + /ByteRange), hashes the signed byte ranges, and sends only that digest. Sigill signs it with the KMS-held key, embeds the RFC 3161 timestamp, and returns the PAdES CMS for the client to embed. On the Business plan and above, the response also carries the certificate-chain, OCSP and CRL DERs needed to build the PDF's Document Security Store locally (PAdES B-LT — CRLs cover the CRL-only CAs that publish no OCSP responder) — lower tiers stop at B-T and receive empty arrays. For B-LTA (Business+), hash the DocTimeStamp ByteRange and stamp it via /tsa/stamp-hash; the response's tokenBase64 is the token to embed. The .NET and Python SDKs implement the whole flow in one call.

Request body (JSON)
{
  "hashHex": "64-char lowercase SHA-256 hex of the PDF ByteRange (required)",
  "certificateId": "uuid (required)",
  "label": "string — stored in operation log (opt)",
  "qualified": "boolean — qualified RFC 3161 timestamp (default false)",
  "tags": [
    "string[] (opt) — up to 10 distinct tags attached at creation; >10 in the request is a 400, inserts stop silently at an evidence's existing 10-tag cap"
  ]
}
Response 200 (JSON)
{
  "cmsBase64": "DER CMS for the /Contents slot",
  "certChainDers": [
    "base64 DER — for the PDF DSS (Business+; empty below)"
  ],
  "ocspDers": [
    "base64 DER OCSP responses — for the PDF DSS (Business+; empty below)"
  ],
  "crlDers": [
    "base64 DER CRLs — for the PDF DSS /CRLs (Business+; empty below)"
  ],
  "operationId": "uuid",
  "certificateId": "uuid",
  "timestampedBy": "TSA name | null",
  "qualified": "boolean",
  "format": "pades-b-t | pades-bes"
}
The recorded document hash for this operation is the ByteRange digest — Sigill never sees the PDF, so lookups match that digest rather than a whole-file hash. Reserve a 16 KiB /Contents slot (the SDKs do). Post-quantum hybrid signing is not offered for PAdES — the profile allows a single SignerInfo; use detached CAdES or JAdES for ML-DSA-87 hybrid seals.

With Store PAdES seal data enabled in Settings, the final returned CMS is also escrowed and re-downloadable via GET /seal/operations/:id/p7s — insurance if your pipeline loses the response before embedding, and a detached proof object. The CMS holds the signature, certificate chain and ByteRange digest — plus the signature timestamp when the operation completed as pades-b-t — never document content. Off by default.

400 if hashHex is malformed; 404 if the certificate is unknown, inactive, or not visible to the tenant.
GET/seal/operationsJWT / API key

Paginated seal history for the authenticated tenant.

Query params
{
  "page": 1,
  "pageSize": 50,
  "search": "label prefix or hash prefix (opt)"
}
Response 200
{
  "total": 42,
  "page": 1,
  "pageSize": 50,
  "items": [
    {
      "id": "uuid",
      "documentHash": "hex",
      "label": "string",
      "status": "success",
      "createdAt": "ISO 8601",
      "certLabel": "string",
      "signatureType": "pades | cades | jades",
      "hasP7s": "boolean — true when the detached signature is stored and downloadable",
      "tsaName": "string | null"
    }
  ]
}
GET/seal/operations/:idJWT / API key

Single seal operation with the detail the evidence page needs: the sealing certificate's validity window, the ML-DSA-87 hybrid signer certificate when the seal is post-quantum hybrid, the linked signature-timestamp transaction ID, and any linked DocTimeStamp transaction IDs (B-LTA archival tokens). Each transaction's restamp chain is served by /api/transactions/:id/chain.

Response 200
{
  "id": "uuid",
  "documentHash": "hex",
  "label": "string",
  "status": "success",
  "signatureType": "pades | cades | jades",
  "tsaTransactionId": "uuid | null",
  "docTimestampTransactionIds": [
    "uuid"
  ],
  "hasP7s": true,
  "pqc": false,
  "cert": {
    "label": "string",
    "source": "byoc | platform",
    "certNotBefore": "ISO 8601",
    "certNotAfter": "ISO 8601"
  },
  "pqcCert": "same shape | null"
}
GET/seal/operations/:id/p7sJWT / API key

Download the stored signature object for a seal operation — CAdES .p7s (application/pkcs7-signature) or JAdES .jades.json (application/jose+json) when the tenant has Store detached seals enabled, or the escrowed PAdES /Contents CMS (.pades.p7s, both the upload and delegated sealing paths) when Store PAdES seal data is enabled. Note the PAdES CMS signs the signature revision's ByteRange digest: it belongs in the PDF's /Contents slot — it is not a detached signature over the original file bytes.

Response 200
detached signature bytes (binary)
Response 404
{
  "message": "Signature not stored — enable Store detached seals in Settings, or download immediately after sealing."
}
POST/seal/verifypublic

Verify a sealed document. Accepts multipart/form-data. Routes automatically — if a p7s field is present, it is inspected: DER bytes verify as CAdES, JSON verifies as JAdES, both against the original file. Otherwise file is treated as a sealed PDF (PAdES).

Form fields
{
  "file": "original file or sealed PDF (required)",
  "p7s": "detached signature — .p7s (CAdES) or .jades.json (JAdES), auto-detected (opt)",
  "tsr": "standalone .tsr token for external timestamp verification (opt)"
}
Response 200 — PAdES
{
  "format": "pades",
  "pades": {
    "signaturePresent": true,
    "hashMatch": true,
    "certificate": {
      "subject": "CN=Acme Corp Seal",
      "trust": "chained | dev_ca | self_signed",
      "qc": {
        "isEidasQualified": false
      }
    },
    "timestamp": {
      "genTime": "ISO 8601",
      "tsaName": "string",
      "qc": {
        "isEidasQualified": true
      }
    }
  }
}
Response 200 — CAdES
{
  "format": "cades",
  "cades": {
    "signaturePresent": true,
    "hashMatch": true,
    "fileHashHex": "hex",
    "certificate": {
      "subject": "CN=Acme Corp Seal",
      "trust": "chained"
    },
    "timestamp": {
      "genTime": "ISO 8601",
      "tsaName": "string"
    },
    "postQuantum": {
      "present": true,
      "valid": true,
      "algorithm": "ml-dsa-87",
      "signatureValid": true,
      "contentBound": "yes | no | not_checked",
      "trusted": "yes | no | not_evaluated"
    },
    "tsrSource": "embedded | external | null",
    "tsrMatchError": null,
    "error": null
  }
}
Response 200 — JAdES
{
  "format": "jades",
  "jades": {
    "signaturePresent": true,
    "hashMatch": true,
    "signatureValid": true,
    "fileHashHex": "hex",
    "embeddedDigestHex": "hex — sigD.hashV the signer committed to",
    "certificate": {
      "subject": "CN=Acme Corp Seal",
      "trust": "chained"
    },
    "timestamp": {
      "genTime": "ISO 8601",
      "tsaName": "string"
    },
    "tsrSource": "embedded | external | null",
    "error": null
  }
}
Error responses
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "No file provided."
}
POST/seal/verify-hashpublic

Verify a detached CAdES or JAdES seal using content digests only. p7sBase64 carries either artifact — the format is detected automatically. For hybrid PQC seals, include hashHex512 to check the ML-DSA signer's SHA-512 content binding. Without it, the PQC signature can still be checked but postQuantum.contentBound is "not_checked".

Request body
{
  "hashHex": "SHA-256/384/512 hex for the classical signer",
  "hashHex512": "128-char SHA-512 hex for PQC binding (optional)",
  "p7sBase64": "base64 of the detached signature — .p7s or .jades.json (auto-detected)",
  "tsrBase64": "base64 standalone TSR (optional)"
}
Response 200
{
  "format": "cades",
  "cades": {
    "signaturePresent": true,
    "hashMatch": true,
    "signatureValid": true,
    "postQuantum": {
      "present": true,
      "valid": false,
      "algorithm": "ml-dsa-87",
      "signatureValid": true,
      "contentBound": "not_checked",
      "trusted": "no | not_evaluated"
    }
  }
}
POST/seal/verify-objectspublic

Object-level verification of a multi-object JAdES seal, by digests only — the record-keeping verdict. Send the JWS plus a digest per referenced URI (the envelope's digest goes under urn:sigill:envelope like any other object). Returns per-object verdicts and complete — true only when the signature verifies and every signed object was supplied and matched. A partial supply yields an honest "signature valid, record incomplete", never a blended verdict. Distinct from membership checks ("does this digest appear?") — use /seal/verify-hash for those.

Request body
{
  "signature": "the General JWS JSON object (the artifact's signature member)",
  "digests": {
    "urn:sigill:envelope": "64-char hex of the canonical envelope",
    "urn:uuid:…": "64-char hex per referenced object"
  },
  "digests512": {
    "urn:sigill:envelope": "128-char SHA-512 hex — required to fully verify hybrid (pqc) seals",
    "urn:uuid:…": "…"
  },
  "tsrBase64": "base64 standalone TSR (optional)"
}
Response 200
{
  "format": "jades",
  "objects": {
    "signatureValid": true,
    "complete": true,
    "objectCount": 3,
    "suppliedCount": 3,
    "matchedCount": 3,
    "objects": [
      {
        "par": "uri",
        "contentType": "MIME | null",
        "supplied": true,
        "hashMatch": true
      }
    ],
    "missing": [
      "signed URIs you did not supply"
    ],
    "unreferenced": [
      "supplied URIs the signature never signed — flagged, never silently accepted"
    ],
    "pqc": "absent | verified | failed | not_checked",
    "underlying": "signature/certificate/timestamp detail",
    "warnings": [
      "skipped invalid signature entries, if any"
    ]
  }
}
Object claims and the validity verdict always come from the same signature entry — the first cryptographically valid sigD-bearing classical signature; object lists are never merged across signatures. Role coverage ("is a prompt and an output present?") is an envelope-layer check your verifier runs against its own copy of the envelope — Sigill sees URIs and digests, not roles.

Hybrid (pqc) seals are both-required: complete is only reachable when the ML-DSA-87 signer's mirrored SHA-512 commitment is also verified, via digests512. A hybrid seal verified with SHA-256 digests alone reports pqc: "not_checked" and complete: false — the classical verdict never masquerades as full hybrid verification.

MCP server

POST/mcpOAuth / API key

Model Context Protocol server (Streamable HTTP, JSON-RPC 2.0) for AI assistants and agents. Stateless: protocol 2026-07-28 (per-request metadata, server/discover) and the earlier initialize-based versions 2025-11-25, 2025-06-18 and 2025-03-26; no sessions, no GET stream. Every tool declares a title, read-only/destructive hints and an output schema, and returns structured content alongside text.

Connect from ChatGPT or Claude: add https://api.sigill.ai/mcp as a custom connector and sign in when asked. You are sent to a Sigill consent page; approving connects the assistant to your current organisation with the permissions shown (read evidence and usage, create timestamps, create seals — and, only if you tick it, organise evidence). Manage or disconnect it under Settings → Connected apps; owners see and can disconnect their members' connections, and removing a member ends theirs immediately. Scripts and Claude Code can keep passing an API key as Authorization: Bearer <key>. sigill_verify, sigill_inspect, sigill_lookup, sigill_verify_seal_hash and sigill_verify_objects work without signing in.

OAuth details (for client developers): authorization code with PKCE (S256), public clients only. Discovery at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server; client ID metadata documents, or dynamic registration at POST /oauth/register with redirect URIs limited to known assistant callbacks and loopback /callback; GET /oauth/authorize, POST /oauth/token, POST /oauth/revoke. Scopes mcp:read, mcp:stamp, mcp:seal, mcp:evidence:write (opt-in), offline_access. Access tokens last one hour and are valid only at /mcp; refresh tokens rotate on every use, and reusing an old one disconnects the app.

No document content. The MCP server has no document upload or download capability, in either direction. Tools take hashes, evidence tokens (timestamp tokens, detached signatures) and ordinary text arguments such as labels and tags, and return the same. Hash files locally (sha256sum, Get-FileHash) and pass the hex digest. Detached seals (CAdES .p7s, JAdES) are created and verified over the digest; save the returned signature next to the file. Embedded PDF (PAdES) seals need the document, so they are not available over MCP — use the SDKs (delegated PAdES, the PDF still stays local), the web app or the seal endpoints.

Tools
sigill_stamp              timestamp a hex digest — TSA choice,
                          second digest, reminders, tags (key)
sigill_verify             digest + TSR → match / signature check
sigill_inspect            TSR metadata
sigill_lookup             public existence check (opt-in records)
sigill_list_tsas          authorities you can choose (key)
sigill_seal_hash          detached CAdES/JAdES seal over a digest,
                          signature returned inline; timestamped
                          when a TSA responds (key)
sigill_verify_seal_hash   verify a detached seal by digest
sigill_seal_objects       multi-object JAdES seal by digests,
                          ≤32 objects, URIs ≤256 chars (key)
sigill_verify_objects     verify a multi-object seal by digests
sigill_list_operations    recent seal operations + level (key)
sigill_list_certificates  seal certificates (key)
sigill_usage              plan, quotas, trials, features (key)

Evidence store (key):
sigill_list_evidence      list/filter evidence; ids, hashes, links
sigill_get_evidence       restamp chain or seal detail
sigill_evidence_events    custody event log
sigill_list_validation_reports / sigill_get_validation_report
                          ETSI TS 119 102-2 reports (XML)
sigill_export_evidence_record   RFC 4998 .ers (base64)
sigill_get_timestamp_token      stored .tsr (base64)
sigill_tag_evidence · sigill_label_evidence · sigill_archive_evidence
sigill_set_evidence_reminders · sigill_verify_evidence
sigill_revalidate_evidence · sigill_list_renewal_jobs
sigill_restamp_evidence · sigill_reseal_evidence  (dry run by default)
search · fetch            evidence search for ChatGPT deep research
Limits
request body   256 KB
rate           per organisation, and a smaller
               shared limit for unauthenticated calls
               → HTTP 429, code "rate_limited"
Changed: the file-transfer tools sigill_create_upload, sigill_seal_uploaded_file and sigill_verify_uploaded_seal and their upload/download URLs have been removed; calling them is now a protocol error (Unknown tool). Use sigill_seal_hash / sigill_verify_seal_hash for detached seals, and the SDKs or the web app for PDFs.

Code examples

Stamp a file with curl

# 1. Hash your file locally — the file never leaves your machine
HASH=$(sha256sum yourfile.pdf | awk '{print $1}')

# 2. Stamp the hash — "auto" uses Sigill-managed standard
# RFC 3161 timestamping with redundancy and failover.
curl -X POST https://api.sigill.ai/tsa/stamp-hash \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "hashHex": "'"$HASH"'",
    "tsaSlug": "auto",
    "label": "yourfile.pdf"
  }' | jq .

# 3. Save the .tsr token
curl -X POST https://api.sigill.ai/tsa/stamp-hash \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hashHex":"'"$HASH"'","tsaSlug":"auto"}' \
  | jq -r .tsrBase64 | base64 -d > yourfile.tsr

Stamp a file with Python

import hashlib, base64, httpx

API_KEY = "your_api_key"
BASE_URL = "https://api.sigill.ai"

# Hash the file locally — it never leaves your machine
with open("yourfile.pdf", "rb") as f:
    hash_hex = hashlib.sha256(f.read()).hexdigest()

resp = httpx.post(
    f"{BASE_URL}/tsa/stamp-hash",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        # "auto" uses Sigill-managed standard RFC 3161 timestamping.
        # Use a verified BYOT reference when you need a customer-managed TSA endpoint.
        "hashHex": hash_hex,
        "tsaSlug": "auto",
        "label": "yourfile.pdf",
    },
)
resp.raise_for_status()
data = resp.json()

# Save the .tsr token
tsr_bytes = base64.b64decode(data["tsrBase64"])
with open("yourfile.tsr", "wb") as f:
    f.write(tsr_bytes)

# data["tsaName"] reports timestamp token issuer metadata or your BYOT label
print(f"Stamped at {data['genTime']} by {data['tsaName']}")

Stamp a file with Node.js

import fs from "fs";
import crypto from "crypto";
import fetch from "node-fetch";

const API_KEY = "your_api_key";
const BASE_URL = "https://api.sigill.ai";

// Hash the file locally — it never leaves your machine
const hashHex = crypto
  .createHash("sha256")
  .update(fs.readFileSync("yourfile.pdf"))
  .digest("hex");

const res = await fetch(`${BASE_URL}/tsa/stamp-hash`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    // "auto" = Sigill-managed standard timestamping with failover. Recommended for production.
    hashHex,
    tsaSlug: "auto",
    label:   "yourfile.pdf",
  }),
});

if (res.status === 502) {
  // All TSAs failed — inspect the structured failure list
  const err = await res.json();
  console.error(`${err.attemptsTried} TSA(s) failed:`, err.failures);
  process.exit(1);
}

const data = await res.json();

// Save the .tsr token
const tsr = Buffer.from(data.tsrBase64, "base64");
fs.writeFileSync("yourfile.tsr", tsr);

console.log(`Stamped at ${data.genTime} by ${data.tsaName}`);

Seal a non-PDF file (CAdES) with curl

For PDFs the response is a signed PDF; for any other file you get a .p7s detached signature. Check X-Seal-Format to branch.

# Seal any file — PDF gets PAdES, everything else gets CAdES .p7s
CERT_ID="<your-certificate-uuid>"
FILE="report.json"

RESPONSE=$(curl -s -D - -o /tmp/sealed_output   -X POST https://api.sigill.ai/seal/sign   -H "Authorization: Bearer YOUR_API_KEY"   -F "file=@$FILE"   -F "certificateId=$CERT_ID"   -F "label=$FILE"   -F "qualified=false")

FORMAT=$(echo "$RESPONSE" | grep -i "x-seal-format:" | tr -d '
' | awk '{print $2}')

if [ "$FORMAT" = "cades" ]; then
  # Non-PDF: response is a detached .p7s signature
  # Keep both the original file and the .p7s — you need both to verify
  mv /tmp/sealed_output "${FILE%.json}.p7s"
  echo "CAdES seal saved: ${FILE%.json}.p7s"
  echo "Keep alongside original: $FILE"
else
  # PDF: response is the sealed PDF with embedded signature
  mv /tmp/sealed_output "${FILE%.pdf}_sealed.pdf"
  echo "PAdES seal saved: ${FILE%.pdf}_sealed.pdf"
fi

Seal a non-PDF file (CAdES) with Python

import httpx, pathlib

API_KEY  = "your_api_key"
BASE_URL = "https://api.sigill.ai"
CERT_ID  = "<your-certificate-uuid>"
file_path = pathlib.Path("report.json")

with open(file_path, "rb") as fh:
    resp = httpx.post(
        f"{BASE_URL}/seal/sign",
        headers={"Authorization": f"Bearer {API_KEY}"},
        files={"file": (file_path.name, fh, "application/octet-stream")},
        data={
            "certificateId": CERT_ID,
            "label": file_path.name,
            "qualified": "false",
        },
        timeout=60,
    )

resp.raise_for_status()
fmt = resp.headers.get("x-seal-format", "pades")

if fmt == "cades":
    # Non-PDF: detached CAdES .p7s — store alongside the original file
    out = file_path.with_suffix(".p7s")
    out.write_bytes(resp.content)
    print(f"CAdES seal → {out}")
    print(f"Keep original file: {file_path}")
    print(f"Verify: POST /seal/verify with file={file_path.name} + p7s={out.name}")
else:
    # PDF: sealed PDF with embedded PAdES signature
    out = file_path.with_stem(file_path.stem + "_sealed").with_suffix(".pdf")
    out.write_bytes(resp.content)
    print(f"PAdES seal → {out}")

Verify a CAdES seal with curl

# Verify a CAdES seal — pass original file + .p7s
# The server checks the signature and returns structured JSON

curl -s -X POST https://api.sigill.ai/seal/verify \
  -F "file=@report.json" \
  -F "p7s=@report.p7s" \
  | jq '{
      format: .format,
      intact: .cades.hashMatch,
      signer: .cades.certificate.subject,
      timestamp: .cades.timestamp.genTime,
      tsa: .cades.timestamp.tsaName,
      qualified: .cades.timestamp.qc.isEidasQualified
    }'

# To also verify the embedded timestamp against the standalone .tsr:
curl -s -X POST https://api.sigill.ai/seal/verify \
  -F "file=@report.json" \
  -F "p7s=@report.p7s" \
  -F "tsr=@report.tsr" \
  | jq '.cades.tsrSource'  # → "external" when .tsr matched

Seal & verify JSON data (JAdES) with curl

JAdES is the ETSI signature format for JSON — ideal for API payloads, structured records, and AI conversation logs. Only the hash leaves your machine.

# Seal JSON data as JAdES (ETSI TS 119 182-1) — hash-only, the file never leaves your machine
CERT_ID="<your-certificate-uuid>"
FILE="conversation.json"
HASH=$(sha256sum "$FILE" | awk '{print $1}')

curl -s -X POST https://api.sigill.ai/seal/sign-hash \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"hashHex\":\"$HASH\",\"certificateId\":\"$CERT_ID\",\"label\":\"$FILE\",\"format\":\"jades\",\"contentType\":\"application/json\"}" \
  -o "${FILE%.json}.jades.json"

# Verify — original file + detached .jades.json (same endpoint as CAdES)
curl -s -X POST https://api.sigill.ai/seal/verify \
  -F "file=@$FILE" \
  -F "p7s=@${FILE%.json}.jades.json" \
  | jq '{format, intact: .jades.hashMatch, signer: .jades.certificate.subject}'

# NOTE: the seal covers the exact bytes of $FILE.
# Re-exporting or pretty-printing the JSON breaks it — by design.