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.

Authentication

POST/auth/registerpublic

Create a new user account.

Request body
{
  "name": "string",
  "email": "string",
  "password": "string (min 8)"
}
Response 200
{
  "token": "<jwt>",
  "userId": "uuid",
  "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

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": "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"
}
Standard stamps use round-robin with automatic failover across your enabled TSAs when 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."
}
POST/tsa/restampJWT / API key

Archival 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.
POST/tsa/verify-hashpublic

Verify 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
  }
}
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 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 (enabledTsaSlugs in 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
    }
  ]
}
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), 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.

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

GET/api/transactions/detailsJWT / API key

Paginated 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": [
    "..."
  ]
}
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"
}

TSA Providers

GET/proxy/servicespublic

List 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
  }
]
NameSlugHasheIDAS qualified
DigiCertdigicertSHA-256No
SectigosectigoSHA-512Qualified endpoint available
GlobalSignglobalsignSHA-512No
SwissSignswisssignSHA-512No
ai.moda RFC3161rfc3161.ai.modaSHA-512No

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.

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.

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": "-----BEGIN CERTIFICATE-----\n…leaf + intermediates…\n-----END CERTIFICATE-----"
}
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 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"
}
PDF files — response body is the sealed PDF (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.
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",
      "hasP7s": "boolean — true when .p7s is stored and downloadable",
      "tsaName": "string | null"
    }
  ]
}
GET/seal/operations/:id/p7sJWT / API key

Download 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."
}
POST/seal/verifypublic

Verify 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 seal

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" 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.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 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"
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