Official clients for Python and JavaScript/TypeScript. Attest, verify, and encrypt with a single function call.
CertiSigma provides three-tier cryptographic attestation for any SHA-256 hash:
The SDK handles hashing, request signing, error handling, and optional client-side encryption. You send a hash (or a file), the SDK does the rest.
Sync and async clients built on httpx. Python 3.10+.
pip install certisigma
For client-side encryption support (AES-256-GCM):
pip install certisigma[crypto]
Pass your API key to the client constructor. The SDK sends it as a Bearer token on every request.
import os
from certisigma import CertiSigmaClient
client = CertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"])
cs_live_... for production, cs_demo_... for sandbox.
Never hard-code keys — use environment variables or a secrets manager.
If your admin has configured an IP Allowlist on your key, requests from non-whitelisted IPs will receive 403 Forbidden. See IP Allowlist docs.
from certisigma import CertiSigmaClient, hash_file, hash_bytes
client = CertiSigmaClient(api_key="cs_live_your_key_here")
# 1. Compute the SHA-256 hash of your file
file_hash = hash_file("contract.pdf")
print(f"SHA-256: {file_hash}") # 64-char lowercase hex
# 2. Attest — creates a timestamped, signed proof of existence
result = client.attest(file_hash, source="my-app")
print(f"Attestation: {result.id} at {result.timestamp}")
print(f"ECDSA signature: {result.signature}")
# 3. Verify — confirm the hash was attested
check = client.verify(file_hash)
print(f"Exists: {check.exists}, Level: {check.level}")
# Or hash raw bytes directly
data_hash = hash_bytes(b"any raw content")
result2 = client.attest(data_hash)
Use AsyncCertiSigmaClient for non-blocking I/O in asyncio applications.
import asyncio
from certisigma import AsyncCertiSigmaClient, hash_file
async def main():
# hash_file() is sync (CPU-bound) — call before entering async context
file_hash = hash_file("contract.pdf")
async with AsyncCertiSigmaClient(api_key="cs_live_xxx") as client:
result = await client.attest(file_hash, source="async-pipeline")
print(f"{result.id} — {result.hash_hex}")
check = await client.verify(file_hash)
print(f"Exists: {check.exists}")
asyncio.run(main())
A production integration typically follows this pattern:
import json, sqlite3
from certisigma import CertiSigmaClient, hash_file
client = CertiSigmaClient(api_key="cs_live_xxx")
# 1. Hash the file locally (SHA-256, streamed in 8 KiB chunks)
hash_hex = hash_file("contract.pdf")
# 2. Attest
result = client.attest(hash_hex, source="contract-pipeline")
# 3. Save to your local database
db = sqlite3.connect("attestations.db")
db.execute("""INSERT INTO attestations
(id, hash_hex, timestamp, signature, source, file_path)
VALUES (?, ?, ?, ?, ?, ?)""",
(result.id, result.hash_hex, result.timestamp,
result.signature, "contract-pipeline", "contract.pdf"))
db.commit()
# 4. Later — download the full cryptographic evidence
evidence = client.get_evidence(result.id)
print(f"Level: {evidence.level}") # T0, T1, or T2
# 5. Save evidence as a local manifest (include T2 when available)
with open("contract.pdf.certisigma.json", "w") as f:
json.dump({
"id": result.id,
"hash": result.hash_hex,
"timestamp": result.timestamp,
"signature": result.signature,
"level": evidence.level,
"t1": evidence.t1,
"t2": evidence.t2,
}, f, indent=2)
| Field | Type | Description |
|---|---|---|
id | str | Attestation ID (att_...) |
hash_hex | str | SHA-256 hash that was attested |
timestamp | str | ISO 8601 creation time |
signature | str | Base64 ECDSA P-256 signature |
status | str | created (new) or existing (already attested) |
etag | str | ETag for optimistic concurrency on metadata updates |
claim_id | int | Your API key's claim ID for this attestation |
source | str | Source label you provided |
extra_data | dict | Metadata you attached |
Always persist the attestation result in your own database. Recommended SQLite schema:
CREATE TABLE IF NOT EXISTS attestations (
id TEXT PRIMARY KEY,
hash_hex TEXT NOT NULL,
timestamp TEXT NOT NULL,
signature TEXT NOT NULL,
source TEXT,
original_file TEXT,
level TEXT DEFAULT 'T0',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
.certisigma.json file alongside each attested file.
This creates an offline-verifiable record without querying the API.
Attest or verify up to 100 hashes in a single API call. Efficient for bulk workloads.
from certisigma import hash_file
import glob
# Compute hashes for all invoices
files = glob.glob("invoices/*.pdf")
hashes = [hash_file(f) for f in files]
# Attest up to 100 hashes in one call
batch = client.batch_attest(hashes, source="monthly-invoices")
print(f"Created: {batch.created}, Existing: {batch.existing}")
# Later — verify the same files haven't been tampered with
current_hashes = [hash_file(f) for f in files]
results = client.batch_verify(current_hashes)
print(f"Found: {results.found}/{results.count}")
Hash any local file and attest the SHA-256 in one call.
result = client.attest_file("/path/to/document.pdf", source="uploads")
print(f"Attested: {result.hash_hex}")
Standalone SHA-256 functions to compute hashes without attestation. Useful for pre-computing hashes, local verification, batch preparation, or audit workflows.
from certisigma import hash_file, hash_bytes
# Hash a file (streamed in 8 KiB chunks — constant memory)
file_hash = hash_file("/path/to/document.pdf")
print(f"SHA-256: {file_hash}") # 64-char hex
# Hash raw bytes
data_hash = hash_bytes(b"raw content")
# Verify a file against a known hash
assert hash_file("/path/to/document.pdf") == file_hash
# Pre-compute hashes for batch attestation
hashes = [hash_file(p) for p in file_paths]
batch = client.batch_attest(hashes, source="bulk-ingest")
| Function | Input | Returns |
|---|---|---|
hash_file(path) | str | Path | 64-char hex SHA-256 |
hash_bytes(data) | bytes | bytearray | memoryview | 64-char hex SHA-256 |
Each API key owns its own claim on an attestation. Claims hold source (a label)
and extra_data (a JSON object). Multiple API keys can independently claim the same hash.
# Update claim metadata
result = client.update_metadata(
"att_1234",
source="pipeline-v2",
extra_data={"project": "alpha", "version": "2.0"}
)
# Soft-delete claim
client.delete_metadata("att_1234")
# Get full cryptographic evidence
evidence = client.get_evidence("att_1234")
print(evidence.level)
ETag with every attestation.
When updating metadata via PATCH /attestation/{id}/metadata,
send If-Match: <etag> to prevent conflicting updates. On mismatch, the API returns 412 Precondition Failed.
Encrypt metadata with AES-256-GCM before sending. The server never sees plaintext —
true zero-knowledge storage. Requires pip install certisigma[crypto].
from certisigma.crypto import generate_key, encrypt_metadata, decrypt_metadata
# Generate a key (store securely — server never sees it)
key = generate_key()
# Encrypt before sending
encrypted = encrypt_metadata({"secret": "classified"}, key)
result = client.attest(hash_hex, extra_data=encrypted, client_encrypted=True)
# Decrypt after retrieving
check = client.verify(hash_hex)
plaintext = decrypt_metadata(check.extra_data, key)
Available crypto functions: generate_key(), encrypt_metadata(),
decrypt_metadata(), is_encrypted(), rotate_key().
# Verify a single hash
check = client.verify("e3b0c44...")
if check.exists:
print(f"Attested at {check.timestamp}, Level: {check.level}")
# Verify from a saved manifest
import json
with open("contract.pdf.certisigma.json") as f:
manifest = json.load(f)
check = client.verify(manifest["hash"])
print(f"Level: {check.level}")
# Batch verify (up to 100)
results = client.batch_verify(["aabb...", "ccdd..."])
for item in results.results:
print(f"{item.hash_hex}: {item.exists}")
Verification endpoints are public — anyone can verify an attestation without an API key. This enables third-party audits, compliance checks, and independent verification without sharing credentials.
from certisigma import CertiSigmaClient, hash_file
# No api_key needed — public verification
client = CertiSigmaClient()
file_hash = hash_file("contract.pdf")
check = client.verify(file_hash)
print(f"Exists: {check.exists}, Level: {check.level}")
# Batch verify also works without a key
results = client.batch_verify([file_hash])
print(f"Found: {results.found}/{results.count}")
Only verify, batch_verify, and health work without a key. Calling attest, batch_attest, update_metadata, or delete_metadata without an api_key raises AuthenticationError immediately — the request is never sent.
The API enforces per-key rate limits using a sliding window:
| Limit | Default | Notes |
|---|---|---|
| Requests / minute | 1,000 | Configurable per key by admin |
| Monthly quota | Unlimited | Optional, per plan |
| Batch max size | 100 | Hashes per batch call |
When rate-limited, the API returns 429 Too Many Requests with these headers:
Retry-After: 60X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetThe SDK automatically raises RateLimitError with a retry_after property.
import time
from certisigma import RateLimitError
def attest_with_backoff(client, hash_hex, max_retries=3):
for attempt in range(max_retries):
try:
return client.attest(hash_hex)
except RateLimitError as e:
wait = e.retry_after * (2 ** attempt)
time.sleep(wait)
raise RuntimeError("Max retries exceeded")
Attestations progress through levels asynchronously. Two patterns to track upgrades:
import time
def wait_for_t1(client, att_id, timeout=600):
start = time.time()
while time.time() - start < timeout:
status = client.status(att_id)
if status.level in ("T1", "T2"):
return status
time.sleep(30)
raise TimeoutError("T1 not reached")
Register a webhook via the API (POST /webhook/register) to get notified when an attestation reaches T1 or T2.
| Event | When | Payload includes |
|---|---|---|
t1_complete | TSA timestamp obtained | attestation_id, hash_hex, tsa_timestamp |
t2_complete | Bitcoin anchor confirmed | attestation_id, hash_hex, bitcoin_block, confirmed_at |
Deliveries are signed with HMAC-SHA256. Verify the signature to ensure authenticity:
import hmac, hashlib
def verify_webhook(body_bytes: bytes, secret: str, signature_header: str) -> bool:
"""Verify CertiSigma webhook HMAC-SHA256 signature."""
expected = "sha256=" + hmac.new(
secret.encode(), body_bytes, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
Best practice: Use webhooks as the primary notification channel and polling as a fallback for missed events.
Once an attestation reaches T2, the Merkle root has been anchored to the Bitcoin blockchain
via OpenTimestamps.
Use get_evidence() to retrieve the full cryptographic bundle, then use the SDK helpers
to inspect the Bitcoin block or save the raw .ots proof file.
t2_complete webhook (or poll with status())get_evidence(att_id).ots proof file for independent verificationfrom certisigma import CertiSigmaClient, get_blockchain_url, save_ots_proof
import json
client = CertiSigmaClient(api_key="cs_live_xxx")
# Retrieve the full evidence bundle
evidence = client.get_evidence("att_1234")
print(f"Level: {evidence.level}") # "T0", "T1", or "T2"
if evidence.level == "T2":
# Get the Bitcoin block explorer link
block_url = get_blockchain_url(evidence) # mempool.space/block/...
tx_url = get_blockchain_url(evidence, type="tx") # mempool.space/tx/...
print(f"Block: {block_url}")
print(f"Tx: {tx_url}")
print(f"Height: {evidence.t2['bitcoinBlockHeight']}")
# Save the raw .ots proof (for independent OTS verification)
save_ots_proof(evidence, "contract.pdf.ots")
# Update your local manifest
with open("contract.pdf.certisigma.json") as f:
manifest = json.load(f)
manifest["level"] = evidence.level
manifest["t2"] = evidence.t2
with open("contract.pdf.certisigma.json", "w") as f:
json.dump(manifest, f, indent=2)
| Field | Type | Description |
|---|---|---|
hash_hex | str | SHA-256 hash of the attested data |
level | str | Highest confirmed level: T0, T1, or T2 |
| t0 — ECDSA Signature (immediate) | ||
t0.signature | str | Base64-encoded ECDSA P-256 signature |
t0.algorithm | str | ECDSA-P256-SHA256 |
t0.publicKeyId | str | Signing key identifier for key pinning |
t0.registeredAt | str | ISO 8601 timestamp of registration |
| t1 — TSA Timestamp + Merkle Proof (minutes) | ||
t1.batchId | int | T1 batch identifier |
t1.merkleRoot | str | Merkle root hash of the batch |
t1.merkleProof | list | Merkle inclusion proof path (leaf → root) |
t1.tsaProvider | str | TSA provider (e.g. sectigo_qualified) |
t1.tsaTimestamp | str | RFC 3161 timestamp (ISO 8601) |
t1.tsaToken | str | Base64 TSA token for independent verification |
| t2 — Bitcoin Anchor via OpenTimestamps (hours) | ||
t2.dailyRoot | str | Daily Merkle root anchored to Bitcoin |
t2.t1ToT2Proof | list | Merkle proof linking T1 batch root to T2 daily root |
t2.bitcoinBlockHeight | int | Bitcoin block number containing the anchor |
t2.bitcoinBlockHash | str | Bitcoin block hash |
t2.bitcoinTxid | str | Bitcoin transaction ID |
t2.confirmedAt | str | ISO 8601 confirmation timestamp |
t2.otsProof | str | Raw OpenTimestamps proof (Base64-encoded binary) |
| Function | Input | Returns |
|---|---|---|
get_blockchain_url(evidence) | EvidenceResult | mempool.space block URL, or None |
get_blockchain_url(evidence, type="tx") | EvidenceResult | mempool.space transaction URL, or None |
save_ots_proof(evidence, path) | EvidenceResult, str | True if saved, False if T2 not ready |
The saved .ots file is a standard
OpenTimestamps proof.
Verify it independently — no CertiSigma involvement needed:
# Install the OpenTimestamps client
pip install opentimestamps-client
# Verify the proof against the Bitcoin blockchain
ots verify contract.pdf.ots
# Or verify online at https://opentimestamps.org
.ots proof ties directly to a Bitcoin block — immutable, decentralized, permanent.
get_blockchain_url(evidence) to get a direct link to
mempool.space, where anyone can independently
verify the Bitcoin block and transaction containing your anchor.
from certisigma import (
CertiSigmaError,
AuthenticationError,
RateLimitError,
QuotaExceededError,
)
try:
client.attest(hash_hex)
except AuthenticationError:
print("Invalid API key")
except RateLimitError as e:
print(f"Rate limited, retry after {e.retry_after}s")
except QuotaExceededError:
print("Monthly quota reached")
except CertiSigmaError as e:
print(f"API error {e.status_code}: {e}")
CERTISIGMA_API_KEY, never in code.encrypt_metadata() for sensitive data in extra_data.rotate_key(envelope, old_key, new_key) to re-encrypt without decrypting on the server.signing_secret in a secrets manager. It is shown only once.| Parameter | Default | Description |
|---|---|---|
api_key | None | Bearer token (cs_live_... or cs_demo_...). Optional for verify/health. |
base_url | https://api.certisigma.ch | API endpoint |
timeout | 30.0 | Request timeout in seconds |
httpx >= 0.25.0cryptography >= 44.0.0 (optional, for certisigma.crypto)PyPI certisigma — MIT License — Ten Sigma Sagl
get_blockchain_url(evidence) — returns a mempool.space URL for the Bitcoin block or transactionsave_ots_proof(evidence, path) — saves the raw OpenTimestamps .ots proof to a local fileapi_key is now optional — public endpoints (verify, batch_verify, health) work without authenticationAuthenticationError raised immediately if auth-required method called without key (no request sent)hash_file() and hash_bytes() utility functionsCertiSigmaClient) and async (AsyncCertiSigmaClient) clientsAuthenticationError, RateLimitError, QuotaExceededErrorpy.typed marker)CertiSigma provides three-tier cryptographic attestation for any SHA-256 hash:
The SDK handles hashing, request signing, error handling, and optional client-side encryption. You send a hash (or a file), the SDK does the rest.
Zero dependencies. Works in Node.js 18+ and modern browsers (native fetch).
npm install @certisigma/sdk
Full TypeScript definitions included (src/index.d.ts, src/crypto.d.ts).
Pass your API key to the client constructor. The SDK sends it as a Bearer token on every request.
const { CertiSigmaClient } = require('@certisigma/sdk');
const client = new CertiSigmaClient({
apiKey: process.env.CERTISIGMA_API_KEY,
});
cs_live_... for production, cs_demo_... for sandbox.
Never hard-code keys — use environment variables or a secrets manager.
If your admin has configured an IP Allowlist on your key, requests from non-whitelisted IPs will receive 403 Forbidden. See IP Allowlist docs.
const { CertiSigmaClient, hashFile, hashBytes } = require('@certisigma/sdk');
const fs = require('fs');
const client = new CertiSigmaClient({ apiKey: 'cs_live_your_key_here' });
// 1. Compute the SHA-256 hash of your file
const fileHash = await hashFile(fs.readFileSync('contract.pdf'));
console.log(`SHA-256: ${fileHash}`); // 64-char lowercase hex
// 2. Attest — creates a timestamped, signed proof of existence
const result = await client.attest(fileHash, { source: 'my-app' });
console.log(`Attestation: ${result.id} at ${result.timestamp}`);
// 3. Verify — confirm the hash was attested
const check = await client.verify(fileHash);
console.log(`Exists: ${check.exists}, Level: ${check.level}`);
// Or hash raw bytes directly
const dataHash = await hashBytes(new TextEncoder().encode('any raw content'));
const result2 = await client.attest(dataHash);
A production integration typically follows this pattern:
const { CertiSigmaClient, hashFile } = require('@certisigma/sdk');
const fs = require('fs');
const client = new CertiSigmaClient({ apiKey: process.env.CERTISIGMA_API_KEY });
// 1. Hash the file locally (SHA-256 via Web Crypto API)
const hashHex = await hashFile(fs.readFileSync('contract.pdf'));
// 2. Attest
const result = await client.attest(hashHex, { source: 'contract-pipeline' });
// 3. Save to a local JSON manifest
const manifest = {
id: result.id,
hash: result.hash_hex,
timestamp: result.timestamp,
signature: result.signature,
source: 'contract-pipeline',
};
fs.writeFileSync('contract.pdf.certisigma.json', JSON.stringify(manifest, null, 2));
// 4. Later — download evidence (includes T2 when available)
const evidence = await client.getEvidence(result.id);
console.log(`Level: ${evidence.level}`);
// 5. Update manifest with evidence
manifest.level = evidence.level;
manifest.t1 = evidence.t1;
manifest.t2 = evidence.t2;
fs.writeFileSync('contract.pdf.certisigma.json', JSON.stringify(manifest, null, 2));
| Field | Type | Description |
|---|---|---|
id | string | Attestation ID (att_...) |
hash_hex | string | SHA-256 hash that was attested |
timestamp | string | ISO 8601 creation time |
signature | string | Base64 ECDSA P-256 signature |
status | string | created (new) or existing (already attested) |
etag | string | ETag for optimistic concurrency on metadata updates |
claim_id | number | Your API key's claim ID for this attestation |
source | string | Source label you provided |
extra_data | object | Metadata you attached |
Always persist the attestation result locally. Example JSON manifest pattern:
{
"id": "att_a1b2c3d4e5f6",
"hash": "e3b0c44298fc1c149afbf4c8996fb924...",
"timestamp": "2025-06-15T10:30:00Z",
"signature": "MEUCIQDx...",
"source": "contract-pipeline",
"original_file": "contract.pdf",
"level": "T2",
"t1": { "batchId": 42, "merkleRoot": "c4f3b2...", "tsaTimestamp": "2025-06-15T10:35:00Z" },
"t2": {
"bitcoinBlockHeight": 850123,
"bitcoinBlockHash": "00000000000000000002a7c4...",
"bitcoinTxid": "d4e5f6a7b8c9...",
"confirmedAt": "2025-06-15T14:20:00Z"
}
}
.certisigma.json file alongside each attested file.
This creates an offline-verifiable record without querying the API.
Attest or verify up to 100 hashes in a single API call.
const { hashFile } = require('@certisigma/sdk');
const fs = require('fs');
// Compute hashes for all invoices
const files = fs.readdirSync('invoices/').filter(f => f.endsWith('.pdf'));
const hashes = await Promise.all(
files.map(f => hashFile(fs.readFileSync(`invoices/${f}`)))
);
// Attest up to 100 hashes in one call
const batch = await client.batchAttest(hashes, { source: 'invoice-processor' });
console.log(`Created: ${batch.created}, Existing: ${batch.existing}`);
// Later — verify the same files haven't been tampered with
const currentHashes = await Promise.all(
files.map(f => hashFile(fs.readFileSync(`invoices/${f}`)))
);
const results = await client.batchVerify(currentHashes);
console.log(`Found: ${results.found}/${results.count}`);
Hash a file client-side (via Web Crypto) and attest the SHA-256 in one call. Works with File, Blob, ArrayBuffer, or Uint8Array.
// Browser — from a file input
const file = document.getElementById('fileInput').files[0];
const result = await client.attestFile(file, { source: 'upload-form' });
// Node.js — from a buffer
const buf = fs.readFileSync('document.pdf');
const result2 = await client.attestFile(buf, { source: 'cli-tool' });
Standalone SHA-256 functions to compute hashes without attestation.
Available from the main package or @certisigma/sdk/hash.
const { hashFile, hashBytes } = require('@certisigma/sdk');
// or: const { hashFile, hashBytes } = require('@certisigma/sdk/hash');
// Hash a File, Blob, ArrayBuffer, or Uint8Array
const fileHash = await hashFile(fileInput.files[0]);
console.log(`SHA-256: ${fileHash}`); // 64-char hex
// Hash raw bytes
const dataHash = await hashBytes(new TextEncoder().encode('raw content'));
// Verify a file against a known hash
const currentHash = await hashFile(fileInput.files[0]);
console.assert(currentHash === fileHash);
// Node.js — hash a local file for batch preparation
const fs = require('fs');
const hashes = await Promise.all(
filePaths.map(p => hashBytes(fs.readFileSync(p)))
);
const batch = await client.batchAttest(hashes, { source: 'bulk-ingest' });
| Function | Input | Returns |
|---|---|---|
hashFile(input) | File | Blob | ArrayBuffer | Uint8Array | Promise<string> — 64-char hex |
hashBytes(data) | Uint8Array | ArrayBuffer | Promise<string> — 64-char hex |
Each API key owns its own claim on an attestation. Claims hold source (a label)
and extraData (a JSON object). Multiple API keys can independently claim the same hash.
// Update claim metadata
await client.updateMetadata('att_1234', {
source: 'pipeline-v2',
extraData: { project: 'alpha', version: '2.0' },
});
// Soft-delete claim
await client.deleteMetadata('att_1234');
// Get full cryptographic evidence
const evidence = await client.getEvidence('att_1234');
ETag with every attestation.
When updating metadata via PATCH /attestation/{id}/metadata,
send If-Match: <etag> to prevent conflicting updates. On mismatch, the API returns 412 Precondition Failed.
Encrypt metadata with AES-256-GCM before sending. The server never sees plaintext — true zero-knowledge storage. Uses the Web Crypto API.
const { generateKey, encryptMetadata, decryptMetadata } = require('@certisigma/sdk/crypto');
// Generate a key (store securely — server never sees it)
const key = await generateKey();
// Encrypt before sending
const encrypted = await encryptMetadata({ secret: 'classified' }, key);
await client.attest(hash, { extraData: encrypted, clientEncrypted: true });
// Decrypt after retrieving
const result = await client.verify(hash);
const plaintext = await decryptMetadata(result.extra_data, key);
Available crypto functions: generateKey(), encryptMetadata(),
decryptMetadata(), isEncrypted(), rotateKey().
// Verify a single hash
const check = await client.verify('e3b0c44...');
if (check.exists) {
console.log(`Attested at ${check.timestamp}, Level: ${check.level}`);
}
// Verify from a saved manifest
const manifest = JSON.parse(fs.readFileSync('contract.pdf.certisigma.json', 'utf8'));
const check2 = await client.verify(manifest.hash);
console.log(`Level: ${check2.level}`);
// Batch verify (up to 100)
const results = await client.batchVerify(['aabb...', 'ccdd...']);
results.results.forEach(r => console.log(`${r.hash_hex}: ${r.exists}`));
Verification endpoints are public — anyone can verify an attestation without an API key. This enables third-party audits, compliance checks, and independent verification without sharing credentials.
const { CertiSigmaClient, hashFile } = require('@certisigma/sdk');
const fs = require('fs');
// No apiKey needed — public verification
const client = new CertiSigmaClient();
const fileHash = await hashFile(fs.readFileSync('contract.pdf'));
const check = await client.verify(fileHash);
console.log(`Exists: ${check.exists}, Level: ${check.level}`);
// Batch verify also works without a key
const results = await client.batchVerify([fileHash]);
console.log(`Found: ${results.found}/${results.count}`);
Only verify, batchVerify, and health work without a key. Calling attest, batchAttest, updateMetadata, or deleteMetadata without an apiKey throws AuthenticationError immediately — the request is never sent.
The API enforces per-key rate limits using a sliding window:
| Limit | Default | Notes |
|---|---|---|
| Requests / minute | 1,000 | Configurable per key by admin |
| Monthly quota | Unlimited | Optional, per plan |
| Batch max size | 100 | Hashes per batch call |
When rate-limited, the API returns 429 Too Many Requests with these headers:
Retry-After: 60X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetThe SDK automatically throws RateLimitError with a retryAfter property.
const { RateLimitError } = require('@certisigma/sdk');
async function attestWithBackoff(client, hashHex, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await client.attest(hashHex);
} catch (err) {
if (err instanceof RateLimitError) {
const wait = err.retryAfter * 1000 * (2 ** i);
await new Promise(r => setTimeout(r, wait));
} else throw err;
}
}
throw new Error('Max retries exceeded');
}
Attestations progress through levels asynchronously. Two patterns to track upgrades:
async function waitForT1(client, attId, timeoutMs = 600000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const s = await client.status(attId);
if (s.level === 'T1' || s.level === 'T2') return s;
await new Promise(r => setTimeout(r, 30000));
}
throw new Error('T1 not reached within timeout');
}
Register a webhook via the API (POST /webhook/register) to get notified when an attestation reaches T1 or T2.
| Event | When | Payload includes |
|---|---|---|
t1_complete | TSA timestamp obtained | attestation_id, hash_hex, tsa_timestamp |
t2_complete | Bitcoin anchor confirmed | attestation_id, hash_hex, bitcoin_block, confirmed_at |
Deliveries are signed with HMAC-SHA256. Verify the signature to ensure authenticity:
const crypto = require('crypto');
function verifyWebhook(bodyBuffer, secret, signatureHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(bodyBuffer)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
Best practice: Use webhooks as the primary notification channel and polling as a fallback for missed events.
Once an attestation reaches T2, the Merkle root has been anchored to the Bitcoin blockchain
via OpenTimestamps.
Use getEvidence() to retrieve the full cryptographic bundle, then use the SDK helpers
to inspect the Bitcoin block or save the raw .ots proof file.
t2_complete webhook (or poll with status())getEvidence(attId).ots proof file for independent verificationconst { CertiSigmaClient, getBlockchainUrl, saveOtsProof } = require('@certisigma/sdk');
const fs = require('fs');
const client = new CertiSigmaClient({ apiKey: process.env.CERTISIGMA_API_KEY });
// Retrieve the full evidence bundle
const evidence = await client.getEvidence('att_1234');
console.log(`Level: ${evidence.level}`); // "T0", "T1", or "T2"
if (evidence.level === 'T2') {
// Get the Bitcoin block explorer link
const blockUrl = getBlockchainUrl(evidence); // mempool.space/block/...
const txUrl = getBlockchainUrl(evidence, 'tx'); // mempool.space/tx/...
console.log(`Block: ${blockUrl}`);
console.log(`Tx: ${txUrl}`);
console.log(`Height: ${evidence.t2.bitcoinBlockHeight}`);
// Save the raw .ots proof (for independent OTS verification)
saveOtsProof(evidence, 'contract.pdf.ots');
// Update your local manifest
const manifest = JSON.parse(fs.readFileSync('contract.pdf.certisigma.json', 'utf8'));
manifest.level = evidence.level;
manifest.t2 = evidence.t2;
fs.writeFileSync('contract.pdf.certisigma.json', JSON.stringify(manifest, null, 2));
}
| Field | Type | Description |
|---|---|---|
hash_hex | string | SHA-256 hash of the attested data |
level | string | Highest confirmed level: T0, T1, or T2 |
| t0 — ECDSA Signature (immediate) | ||
t0.signature | string | Base64-encoded ECDSA P-256 signature |
t0.algorithm | string | ECDSA-P256-SHA256 |
t0.publicKeyId | string | Signing key identifier for key pinning |
t0.registeredAt | string | ISO 8601 timestamp of registration |
| t1 — TSA Timestamp + Merkle Proof (minutes) | ||
t1.batchId | number | T1 batch identifier |
t1.merkleRoot | string | Merkle root hash of the batch |
t1.merkleProof | Array | Merkle inclusion proof path (leaf → root) |
t1.tsaProvider | string | TSA provider (e.g. sectigo_qualified) |
t1.tsaTimestamp | string | RFC 3161 timestamp (ISO 8601) |
t1.tsaToken | string | Base64 TSA token for independent verification |
| t2 — Bitcoin Anchor via OpenTimestamps (hours) | ||
t2.dailyRoot | string | Daily Merkle root anchored to Bitcoin |
t2.t1ToT2Proof | Array | Merkle proof linking T1 batch root to T2 daily root |
t2.bitcoinBlockHeight | number | Bitcoin block number containing the anchor |
t2.bitcoinBlockHash | string | Bitcoin block hash |
t2.bitcoinTxid | string | Bitcoin transaction ID |
t2.confirmedAt | string | ISO 8601 confirmation timestamp |
t2.otsProof | string | Raw OpenTimestamps proof (Base64-encoded binary) |
| Function | Input | Returns |
|---|---|---|
getBlockchainUrl(evidence) | EvidenceResult | mempool.space block URL, or null |
getBlockchainUrl(evidence, 'tx') | EvidenceResult | mempool.space transaction URL, or null |
saveOtsProof(evidence, path) | EvidenceResult, string | true if saved, false if T2 not ready |
The saved .ots file is a standard
OpenTimestamps proof.
Verify it independently — no CertiSigma involvement needed:
# Install the OpenTimestamps client
pip install opentimestamps-client
# Verify the proof against the Bitcoin blockchain
ots verify contract.pdf.ots
# Or verify online at https://opentimestamps.org
.ots proof ties directly to a Bitcoin block — immutable, decentralized, permanent.
getBlockchainUrl(evidence) to get a direct link to
mempool.space, where anyone can independently
verify the Bitcoin block and transaction containing your anchor.
const { AuthenticationError, RateLimitError, QuotaExceededError } = require('@certisigma/sdk');
try {
await client.attest(hash);
} catch (err) {
if (err instanceof AuthenticationError) {
console.error('Invalid API key');
} else if (err instanceof RateLimitError) {
console.error(`Rate limited, retry after ${err.retryAfter}s`);
} else if (err instanceof QuotaExceededError) {
console.error('Monthly quota reached');
}
}
CERTISIGMA_API_KEY, never in source code.encryptMetadata() for sensitive data in extraData.rotateKey(envelope, oldKey, newKey) to re-encrypt without decrypting on the server.signing_secret in a secrets manager. It is shown only once.| Parameter | Default | Description |
|---|---|---|
apiKey | undefined | Bearer token (cs_live_... or cs_demo_...). Optional for verify/health. |
baseUrl | https://api.certisigma.ch | API endpoint |
timeout | 30000 | Request timeout in ms |
fetch supportnpm @certisigma/sdk — MIT License — Ten Sigma Sagl
getBlockchainUrl(evidence) — returns a mempool.space URL for the Bitcoin block or transactionsaveOtsProof(evidence, path) — saves the raw OpenTimestamps .ots proof to a local fileapiKey is now optional — public endpoints (verify, batchVerify, health) work without authenticationAuthenticationError thrown immediately if auth-required method called without key (no request sent)hashFile() and hashBytes() utility functions@certisigma/sdk/hash subpath exportAuthenticationError, RateLimitError, QuotaExceededErrorindex.d.ts, crypto.d.ts)