API Reference
REST API for programmatic timestamping — use your API key from Settings → API Keys
Authentication
/auth/registerpublicCreate a new user account.
Request body
{
"name": "string",
"email": "string",
"password": "string (min 8)"
}Response 200
{
"token": "<jwt>",
"userId": "uuid",
"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
/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": "auto | digicert | sectigo | globalsign | swisssign | rfc3161.ai.moda | skid-ecc | skid-rsa",
"label": "string (optional)",
"force": "boolean — re-stamp an already-stamped hash (default false)",
"qualified": "boolean — request an eIDAS-qualified timestamp (default false)"
}Response 200
{
"serial": "string",
"genTime": "ISO 8601",
"hashAlgorithmOid": "OID string",
"hashHex": "hex string",
"tsrBase64": "base64 .tsr token",
"tsaName": "string — actual TSA used",
"qualified": "boolean",
"policyOid": "string | null — present when qualified: true"
}tsaSlug is "auto". See Automatic TSA selection. Use force: true to create a second independent proof for a file that has already been stamped.Qualified stamps (
qualified: true) route exclusively to eIDAS Qualified Trust Service Providers on the EU Trust List. The default provider is SK ID Solutions using ECC (P-256 / SHA-256withECDSA). Pass tsaSlug: "skid-rsa" for RSA-4096 / SHA-512withRSA if you need compatibility with legacy verifiers. Qualified stamps are counted against a separate monthly quota (5 free / month, all plans). Response 502 — all TSAs failed (auto)
{
"message": "All enabled TSAs failed.",
"attemptsTried": 3,
"failures": [
{
"tsa": "DigiCert",
"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 a new TSA, creating a timestamp chain. Use before a TSA certificate expires.
Request body
{
"transactionId": "uuid — the record to restamp",
"tsaSlug": "auto | digicert | sectigo | globalsign | swisssign | rfc3161.ai.moda"
}Response 200
{
"id": "uuid",
"parentTransactionId": "uuid",
"tsaName": "string",
"genTime": "ISO 8601"
}tsaSlug: "auto" is supported here too. Picking a different TSA than the original stamp is the usual pattern — it broadens the trust base of the resulting chain. /tsa/verify-hashpublicVerify a file against a TSR by its hash. Call /tsa/inspect first to find the hash algorithm recorded in the TSR, compute that hash locally, then pass the hex digest. 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
}
}/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 and Sigill picks a TSA for you. Seal operations via /seal/sign always use this path — you can't pick a TSA there.
How it works
- Round-robin across your enabled TSAs. Each tenant has its own cursor — a stamp picks the next TSA in the list, then the cursor advances. If you have three TSAs enabled, three consecutive stamps will rotate through all of them.
- Failover on the same request. If the chosen TSA fails (network error, timeout, non-2xx status, or an unparseable TSR), the dispatcher tries the next one — and the next — until one succeeds or all are exhausted. The caller only sees the final outcome.
- Honours the tenant allowlist. Only TSAs enabled for your tenant (
enabledTsaSlugsin settings) are candidates. If the list is empty, every active TSA is eligible. - SHA-512 canonical TSQ. Auto mode hashes the file with SHA-512, which every mainstream TSA accepts. Direct slug calls still use each TSA's
preferredHashOid. - Every failure is logged. Errors are stored in the TSA failure log with classification (
network,timeout,http_status,parse) for observability. Tenant admins can see this in the backoffice TSA health panel.
When to use auto
Default to auto in production. You get redundancy at no cost — if one TSA has a bad day, the next one picks up. Reach for an explicit slug only when you have a compliance or legal reason to record that a specific TSA produced the timestamp.
Error response when all TSAs fail
If every candidate TSA fails within the same request, you get a 502 Bad Gateway with a structured list of what was attempted and why:
{
"message": "All enabled TSAs failed.",
"attemptsTried": 3,
"failures": [
{
"tsa": "DigiCert",
"errorClass": "timeout",
"statusCode": null,
"message": "Request timed out after 10s",
"latencyMs": 10042
},
{
"tsa": "Sectigo",
"errorClass": "http_status",
"statusCode": 503,
"message": "service unavailable",
"latencyMs": 412
},
{
"tsa": "GlobalSign",
"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), 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.
Path parameter
hash SHA-512 hex string (128 chars)Response 200
{
"found": true,
"count": 2,
"latest": {
"id": "uuid",
"tsaName": "string",
"genTime": "ISO 8601",
"label": "string|null",
"certNotAfter": "ISO 8601",
"tsrBase64": "base64"
},
"records": [
"..."
]
}Timestamps
/api/transactions/detailsJWT / API keyPaginated list of timestamp records for a tenant, with optional search.
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": [
"..."
]
}/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"
}TSA Providers
/proxy/servicespublicList all active TSA providers and their slugs.
Response 200
[
{
"id": "uuid",
"name": "DigiCert",
"proxySlug": "digicert",
"endpointUrl": "http://timestamp.digicert.com",
"preferredHashOid": "2.16.840.1.101.3.4.2.1",
"isActive": true
}
]| Name | Slug | Hash | eIDAS qualified |
|---|---|---|---|
| DigiCert | digicert | SHA-256 | No |
| Sectigo | sectigo | SHA-512 | Qualified endpoint available |
| GlobalSign | globalsign | SHA-512 | No |
| SwissSign | swisssign | SHA-512 | No |
| ai.moda RFC3161 | rfc3161.ai.moda | SHA-512 | No |
Document Seal
Cryptographic seals backed by KMS — the private key never leaves. PDFs receive a PAdES signature (ETSI EN 319 142-1) embedded in the file. Any other file type receives a detached CAdES signature (ETSI EN 319 122-1) returned as a .p7s file. File type routing is performed server-side via magic-byte inspection. An RFC 3161 timestamp is embedded in every seal automatically. Owner role required to manage certificates; any authenticated user can seal.
/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": "-----BEGIN CERTIFICATE-----\n…leaf + intermediates…\n-----END CERTIFICATE-----"
}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 any file. Accepts multipart/form-data. File type is detected server-side from magic bytes — PDFs produce a PAdES-signed PDF (application/pdf); all other files produce a detached CAdES .p7s (application/pkcs7-signature). An RFC 3161 timestamp is embedded in every seal automatically.
Form fields
{
"file": "any file (required) — PDF → PAdES; other → CAdES .p7s",
"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 — request eIDAS-qualified timestamp (default false)"
}Response headers
{
"X-Seal-Operation-Id": "uuid",
"X-Seal-Certificate-Id": "uuid",
"X-Seal-Timestamped-By": "TSA name | none",
"X-Seal-Format": "pades | cades",
"X-Seal-Qualified": "true | false"
}application/pdf). Save as filename_sealed.pdf.Non-PDF files — response body is a detached CAdES signature (
application/pkcs7-signature). Save as filename.p7s alongside the original file. The .p7s contains the signature and the embedded RFC 3161 timestamp token. Check X-Seal-Format to distinguish the two cases programmatically.TSA selection is automatic — round-robin dispatcher with failover, honouring your tenant's enabled-TSA allowlist. The chosen TSA is returned in
X-Seal-Timestamped-By. If every enabled TSA fails, the seal is still produced but X-Seal-Timestamped-By is none. /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",
"hasP7s": "boolean — true when .p7s is stored and downloadable",
"tsaName": "string | null"
}
]
}/seal/operations/:id/p7sJWT / API keyDownload the stored CAdES .p7s for a seal operation. Only available when signatureType is "cades" and the tenant has Store CAdES seals enabled in Settings. Returns application/pkcs7-signature.
Response 200
.p7s file bytes (binary)Response 404
{
"message": ".p7s not stored — enable Store CAdES 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, performs CAdES verification against the original file; otherwise treats file as a sealed PDF and performs PAdES verification.
Form fields
{
"file": "original file or sealed PDF (required)",
"p7s": ".p7s detached signature — CAdES path only (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"
},
"tsrSource": "embedded | external | null",
"tsrMatchError": null,
"error": null
}
}Error responses
400 no file provided
400 malformed .p7s or PDF
400 file hash does not match sealCode 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" picks a TSA from your tenant's
# enabled list with automatic failover on failure.
# Replace with "sectigo", "digicert", etc. to pin a specific one.
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 round-robin across your enabled TSAs with failover.
# Pin a specific slug (sectigo, digicert, ...) if you need
# a deterministic choice of TSA for compliance reasons.
"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 which TSA actually served this stamp
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" = round-robin + 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 matched