API Reference
REST API for programmatic timestamping — use your API key from Settings → API Keys
Authentication
/auth/registerpublicCreate 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"
}/auth/loginpublicAuthenticate and receive a JWT.
Request body
{
"email": "string",
"password": "string"
}Response 200
{
"token": "<jwt>",
"userId": "uuid",
"email": "string",
"tenantId": "uuid|null"
}/auth/refreshJWTRe-issue a JWT with the latest tenant context. Call after creating an organisation.
Response 200
{
"token": "<new jwt>"
}/auth/meJWTReturn 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.
/tsa/stamp-hashJWT / API keyTimestamp 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",
"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"
}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."
}/tsa/restampJWT / API keyArchival restamp per RFC 3161 §4. Hashes the existing TSR bytes (not the original file) and stamps them with standard RFC 3161 timestamping, creating a timestamp chain. Responses carrying a qualified policy are rejected on this standard-only route. Use before a timestamp authority certificate expires.
Request body
{
"transactionId": "uuid — the record to restamp",
"tsaSlug": "auto | byot:<id-or-slug>"
}Response 200
{
"id": "uuid",
"parentTransactionId": "uuid",
"tsaName": "string",
"genTime": "ISO 8601"
}tsaSlug: "auto" for Sigill-managed standard timestamping. Use a verified BYOT timestamp authority when you need to restamp through a customer-managed TSA endpoint. /tsa/verify-hashpublicVerify 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
}
}/tsa/inspectpublicParse 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 or /tsa/restamp for Sigill-managed standard RFC 3161 timestamping. 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
}
]
}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
/api/lookup/{hash}publicCheck 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"
]
}/api/transactions/{id}/public-lookupJWT · ownerPer-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
/api/transactions/detailsJWT / API keyPaginated 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"
}
]
}/api/transactions/{id}/labelJWT / API keySet or update the label on a timestamp record.
Request body
{
"label": "string (max 255)"
}Response 200
{
"id": "uuid",
"label": "string"
}/api/evidenceJWT / API keyThe 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)
needsAction boolean — only the work queue: expired,
failed verification, or an unacknowledged
expiry alarm
sort lastEvent (default) | horizon (most urgent
first) | lastVerified (stalest first)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,
"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
},
"items": [
{
"hash": "hex",
"alg": "OID string",
"label": "string | null",
"formats": [
"rfc3161",
"cades"
],
"records": 2,
"pqc": 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-t | null",
"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. 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.
/api/evidence/archiveJWT / API keyArchive 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
}/api/transactions/{id}/chainJWT / API keyResolve 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
}
]
}/api/transactions/{id}/chain.zipJWT / API keyDownload 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.
/api/transactions/{id}/audit-package.zipJWT / API keyOne 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/), a SHA-256 checksum manifest over every entry (manifest.json), and a human-readable SUMMARY.txt with step-by-step verification instructions (openssl). The export itself is recorded in the evidence's custody log as evidence.exported.
/api/transactions/{id}/verify-chainJWT / API keyCryptographically 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 restamp covers a digest of the previous TSR bytes (§4.3) — 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"
]
}
]
}/api/evidence/tagsJWT / API keyTag 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": []
}/api/evidence/verifyJWT / API keyVerify 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"
}
]
}/api/evidence/restampJWT / API keyBulk 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./api/evidence/resealJWT / API keyBulk 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
}/api/evidence/jobs/{id}/artifacts.zipJWT / API keyThe 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.
/api/evidence/jobs/{id}JWT / API keyProgress 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"
}
]
}/api/evidence/verify-sweepJWT / API keyRe-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
}/api/transactions/{id}/eventsJWT / API keyChronological 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"
}
]
}/api/transactions/{id}/remindersJWT / API keySet 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"
}/api/transactions/{id}/acknowledge-riskJWTRecord 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
/proxy/servicesJWT / API keyList 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"
}
]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.
/api/tenant/tsaJWTList 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"
}
]
}/api/tenant/tsa/verifyJWTRun 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
}/api/tenant/tsaJWTCreate 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
}/api/tenant/tsa/:idJWTUpdate a BYO TSA. Transport changes trigger a fresh verification before the row is committed.
/api/tenant/tsa/:idJWTRemove a BYO TSA. This remains available even after a plan downgrade so tenants can clean up their configuration.
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.p7swithout the original content ever leaving the client. Same privacy model as/tsa/stamp-hash.format: "jades"— on/seal/signand/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 withPOST /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 and the MCP tools — prefer the hash-only endpoints for integrations.
/seal/certificatesJWT / API keyList 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"
}
]/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.
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-----..."
}/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"
}/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."
}/seal/signJWT / API keySeal 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"
}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. /seal/sign-hashJWT / API keySeal 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"
}.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). /seal/sign-hashesJWT / API keySeal 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, 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)"
]
}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"
}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.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. /seal/sign-pades-hashJWT / API keyDelegated 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 and OCSP DERs needed to build the PDF's Document Security Store locally (PAdES B-LT) — 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)"
],
"operationId": "uuid",
"certificateId": "uuid",
"timestampedBy": "TSA name | null",
"qualified": "boolean",
"format": "pades-b-t | pades-bes"
}/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. /seal/operationsJWT / API keyPaginated 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"
}
]
}/seal/operations/:idJWT / API keySingle 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"
}/seal/operations/:id/p7sJWT / API keyDownload 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."
}/seal/verifypublicVerify 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."
}/seal/verify-hashpublicVerify 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"
}
}
}/seal/verify-objectspublicObject-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"
]
}
}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. 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.tsrStamp 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"
fiSeal 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 matchedSeal & 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.