How It Works

CertiSigma provides three-tier cryptographic attestation for any SHA-256 hash:

  1. T0 — ECDSA Signature — Immediate. The server signs your hash with a P-256 key upon receipt. POST /attest
  2. T1 — TSA Timestamp — Within minutes. Hashes are batched into a Merkle tree and submitted to an RFC 3161 Time Stamping Authority. POST /verify
  3. T2 — Bitcoin Anchor — Within hours. The Merkle root is anchored to the Bitcoin blockchain via OpenTimestamps, providing immutable proof. GET /attestation/{id}/evidence

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.

Installation

Sync and async clients built on httpx. Python 3.10+.

bash
pip install certisigma

For client-side encryption support (AES-256-GCM):

bash
pip install certisigma[crypto]

Authentication

Pass your API key to the client constructor. The SDK sends it as a Bearer token on every request.

python
import os
from certisigma import CertiSigmaClient

client = CertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"])
Key formats: 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.

Quick Start

python
import os
from certisigma import CertiSigmaClient, hash_file, hash_bytes

client = CertiSigmaClient(api_key=os.environ.get("CERTISIGMA_API_KEY"))

# 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)

Async Support

Use AsyncCertiSigmaClient for non-blocking I/O in asyncio applications.

python
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=os.environ.get("CERTISIGMA_API_KEY")) 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())

Integration Contract

ParameterValue
API target/v1/ (all endpoints also available without prefix)
Rate limit1,000 req/min per key (sliding window), Retry-After header on 429
Batch max100 hashes per call (batch_attest, batch_verify)
Public endpointsverify, batch_verify, status, get_evidence, health, /keys, match_derived_list (requires list_key), get_derived_list_signaturebatch_verify(detailed=True) requires API key
Auth-requiredattest, batch_attest, update_metadata, delete_metadata, get_metadata, put_tags, get_tags, query_tags, create_share_token, scan, get_bulk_stats, create_derived_list, list_derived_lists, get_derived_list, get_derived_list_access_log, revoke_derived_list, register_webhook, list_webhooks, delete_webhook, list_webhook_deliveries
RBAC scopesattest, metadata:read, metadata:write, tags:read, tags:write, share, scan, batch, census, webhook — see RBAC & Scoped Keys
Attestation IDsSequential (att_1, att_2, …). Public by design — CertiSigma is a public attestation platform. Cryptographic proof (hash, signature, Merkle, OTS) is intentionally verifiable by any party.
Data isolationOrganizational metadata (source, extra_data, tags) is never exposed on public endpoints. Only the authenticated API key owner can read their own claim data. Public endpoints return only cryptographic proof.
Retry policyCaller-managed. Retry on 429 and 5xx with exponential backoff + jitter
IdempotencyRe-attesting the same hash returns the existing attestation (no duplicate)
Webhook deliveryAt-least-once, ordering not guaranteed, HMAC-SHA256 signed
Support windowCurrent major + previous major for at least 12 months
DeprecationAt least one minor release warning before removal

Recommended Attestation Workflow

A production integration typically follows this pattern:

python
import json, sqlite3
from certisigma import CertiSigmaClient, hash_file

client = CertiSigmaClient(api_key=os.environ.get("CERTISIGMA_API_KEY"))
if not client._api_key:
    raise ValueError("CERTISIGMA_API_KEY environment variable is required")

# 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)

Attest Response Fields

FieldTypeDescription
idstrAttestation ID (att_...)
hash_hexstrSHA-256 hash that was attested
timestampstrISO 8601 creation time
signaturestrBase64 ECDSA P-256 signature
statusstrcreated (new) or existing (already attested)
etagstrETag for optimistic concurrency on metadata updates
claim_idintYour API key's claim ID for this attestation
sourcestrSource label you provided
extra_datadictMetadata you attached

verify() Response Shape

FieldPython typePublic (no key)Authenticated
existsboolAlwaysAlways
hash_hexstrAlwaysAlways
idOptional[str]If existsIf exists
timestampOptional[str]If existsIf exists
levelOptional[str]If existsIf exists
verified_atOptional[str]If existsIf exists
sourceOptional[str]If caller owns a claim

get_evidence() Response Shape

FieldPython typeCondition
idOptional[str]Always
hash_hexstrAlways
levelstrAlways (T0, T1, or T2)
t0dictAlways (keys: formatVersion, registeredAt, signature, publicKeyId)
t1Optional[dict]Present when level ≥ T1 (keys: tsa, timestamp, token)
t2Optional[dict]Present when level = T2 (keys: merkleRoot, bitcoinBlock, txId)
Naming: Both Python and JS SDKs use snake_case for top-level response fields (hash_hex, extra_data, verified_at). Nested objects under t0, t1, t2 use camelCase (formatVersion, registeredAt, publicKeyId).

What to Save After Attestation

Always persist the attestation result in your own database. Recommended SQLite schema:

sql
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
);
Local manifest: Save a .certisigma.json file alongside each attested file. This creates an offline-verifiable record without querying the API.

Minimum Durable Evidence Set (Probative Use)

For long-term legal or audit purposes, persist at least: id, hash_hex, timestamp, signature, level, and when available t1 (TSA + Merkle proof) and t2 (Bitcoin anchor). Save the .ots proof file for independent verification. This set allows verification even if the CertiSigma API is unavailable.

Idempotency

Attest returns status: "created" for new attestations or status: "existing" if the hash was already attested. Safe to retry: duplicate attestations are idempotent. On network timeout, re-attest with the same hash; if it already exists, you get the same attestation ID.

Batch Operations

Attest or verify up to 100 hashes in a single API call. Efficient for bulk workloads.

python
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}")

# Each item includes full claim metadata
for att in batch.attestations:
    print(f"  {att['id']} claim={att['claim_id']} src={att['source']}")

# 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}")

# Detailed mode — certification level + claim metadata (requires api_key)
results = client.batch_verify(current_hashes, detailed=True)
for r in results.results:
    if r["exists"]:
        print(f"  {r['id']} level={r['level']} src={r['source']}")

Batch attest now returns full claim metadata per item (claim_id, source, extra_data, signing_key_id, etag).
Batch verify supports detailed=True for enriched results including certification level and tier timestamps. detailed mode requires API key authentication; without a key the API returns 401.

File Attestation

Hash any local file and attest the SHA-256 in one call.

python
result = client.attest_file("/path/to/document.pdf", source="uploads")
print(f"Attested: {result.hash_hex}")

Hashing Utilities

Standalone SHA-256 functions to compute hashes without attestation. Useful for pre-computing hashes, local verification, batch preparation, or audit workflows.

python
from certisigma import hash_file, hash_bytes, hash_string

# 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")

# Hash a string (UTF-8, no file needed)
str_hash = hash_string("SN-2026-001234")

# Pre-compute hashes for batch attestation
hashes = [hash_file(p) for p in file_paths]
batch = client.batch_attest(hashes, source="bulk-ingest")
FunctionInputReturns
hash_file(path)str | Path64-char hex SHA-256
hash_bytes(data)bytes | bytearray | memoryview64-char hex SHA-256
hash_string(text)str64-char hex SHA-256

String Attestation

Attest identifiers, serial numbers, or any non-file content without creating temporary files. The string is hashed client-side (UTF-8); only the SHA-256 hash reaches the API.

Canonical form: UTF-8, no BOM, no trailing newline, no Unicode normalization. hash_string("SN-001") and hash_string("SN-001\n") produce different hashes. Cross-SDK determinism is guaranteed: Python and JavaScript produce identical hashes for the same input.
python
# One-step: hash + attest
result = client.attest_string("SN-2026-001234", source="serial-registry")

# Verify later
check = client.verify_string("SN-2026-001234")
assert check.exists

# Also works for files
check = client.verify_file("/path/to/document.pdf")
Security note: source is never auto-populated from the string content (unlike attest_file which uses the filename). The string might contain sensitive data — pass source explicitly if needed.
MethodAuth RequiredDescription
attest_string(text, source=, extra_data=)YesHash string + create attestation
verify_string(text)NoHash string + verify attestation
verify_file(path)NoHash file + verify attestation

Metadata and Claims

Each API key owns its own claim on an attestation. Claims hold source (an optional label) and extra_data (a JSON object). Multiple API keys can independently claim the same hash.

source is an optional plaintext operational label (max 100 chars) — designed for system identifiers like pipeline names or app versions, not for sensitive or personal data. If you don't need it, simply omit it. It is never exposed on public endpoints; only you (the authenticated API key owner) can read it. For sensitive metadata, use extra_data with client-side encryption.

python
# 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)
Optimistic concurrency: The API returns an 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.

Client-Side Encryption (Zero Knowledge)

Encrypt metadata with AES-256-GCM before sending. When client_encrypted=true, the server stores extra_data as an opaque blob and never sees plaintext. Requires pip install certisigma[crypto].

What about source? source is always plaintext by design — it's an optional operational label that the backend uses for audit logging and filtering. Encrypting it would make it unusable for those purposes. It is never exposed on public endpoints; only you (the authenticated claim owner) can read it. If you don't need it, omit it entirely. Never store PII in source — use encrypted extra_data instead.
python
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 (extra_data comes from the attest response, not verify)
plaintext = decrypt_metadata(result.extra_data, key)

Available crypto functions: generate_key(), encrypt_metadata(), decrypt_metadata(), is_encrypted(), rotate_key().

Key management: Store encryption keys in a dedicated secrets manager (Vault, AWS KMS, etc.). If you lose the key, the encrypted data is irrecoverable — the server cannot help.

RBAC & Scoped API Keys

API keys carry granular scopes that control access to specific operations. Keys created before RBAC was introduced are automatically backfilled with all scopes for backward compatibility.

ScopeGrants
attestCreate attestations (attest, batch_attest)
batchBatch operations + inventory stats (batch_attest, batch_verify, get_bulk_stats)
metadata:readRead claim metadata (get_metadata)
metadata:writeUpdate/delete metadata (implies metadata:read)
tags:readRead tags (get_tags)
tags:writeCreate/update/delete tags (implies tags:read)
shareCreate and manage share tokens
scanLeak detection (scan) — requires org_id
censusDerived lists (create_derived_list, etc.) — requires org_id
webhookRegister, list, and delete webhooks (register_webhook, list_webhooks, delete_webhook, list_webhook_deliveries)

verify, status, get_evidence, and health are public and require no scope (or no API key at all).

Scope inheritance: metadata:write automatically grants metadata:read; tags:write automatically grants tags:read. You do not need to request both.

Share Tokens (Forensic Metadata Sharing)

Create time-limited, auditable, revocable tokens for sharing attestation metadata with forensic analysts, auditors, or third-party verifiers. Share tokens grant read-only access to a specific set of attestations.

python
token = client.create_share_token(
    attestation_ids=[42, 43, 44],
    expires_in=86400,             # 24 hours (max 30 days)
    recipient_label="forensic-analyst",
    max_uses=10,
)
print(f"Share token: {token.share_token}")  # shown once, save it

# List, inspect, revoke
tokens = client.list_share_tokens()
info = client.get_share_token_info(token.id)
client.revoke_share_token(token.id)
ConstraintValue
Max expiry30 days
Max attestation IDs100 per token
Max usesOptional (unlimited if unset)
Rate limit100 req/min (separate from API key rate limit)
Required scopeshare
Security: Share tokens are stored as SHA-256 hashes — the plaintext token is shown only once at creation. They grant metadata:read and tags:read only for the specified attestation IDs. All access is logged in the share access log.

Structured Tagging

Classify attestations with multi-dimensional key-value tags. Tags support server-side querying (AND semantics) and optional client-side encryption for sensitive values.

python
# Upsert tags (max 50 per attestation per key)
client.put_tags("att_42", tags=[
    {"key": "department", "value": "hr"},
    {"key": "classification", "value": "confidential"},
])

tags = client.get_tags("att_42")

# Query by tags (AND semantics, max 10 conditions)
results = client.query_tags(
    filter={"and": [
        {"key": "department", "value": "hr"},
        {"key": "classification", "value": "confidential"},
    ]},
    limit=100,
)
print(f"Found {results.count} matching attestations")

# Delete a tag
client.delete_tag("att_42", "classification")
ConstraintValue
Max tags per attestation per key50
Tag key format^[a-z][a-z0-9_-]{0,62}$ (lowercase, no _ prefix — reserved)
Max AND conditions per query10
Client-encrypted tagsSupported (value_enc + value_nonce hex), but excluded from server-side query
Required scope (read)tags:read
Required scope (write)tags:write
Tenant isolation: Tags are scoped to your API key. Other keys cannot see or query your tags, even on the same attestation.

Census — Leak Detection & Inventory Stats

Compare suspect hashes against your organization's attested inventory for leak detection, and retrieve aggregate inventory statistics. Both endpoints require an API key with org_id configured.

python
from certisigma import CertiSigmaClient

client = CertiSigmaClient(api_key="cs_live_...")

# Leak detection — compare suspect hashes against org inventory
# Requires `scan` scope + org_id
suspect_hashes = ["a665a459...", "b4c9a289...", "e3b0c442..."]
result = client.scan(suspect_hashes)
print(f"Scan {result.scan_id}: {result.matched}/{result.total_scanned} matched ({result.match_rate:.1%})")
for m in result.matches:
    print(f"  {m.hash_hex} first_seen={m.first_seen} source={m.source}")

# Inventory statistics — aggregate view of your org's claims
# Requires `batch` scope + org_id
stats = client.get_bulk_stats()
print(f"Org {stats.org_id_hash}: {stats.total_claims} claims, {stats.unique_hashes} unique hashes")
print(f"First claim: {stats.first_claim}, Last: {stats.last_claim}")
for month, count in stats.claims_by_month.items():
    print(f"  {month}: {count} claims")

Scan uses a temp-table set intersection for performance on large inputs. The maximum number of hashes per scan is configured server-side (default 50K). Results include source and first_seen timestamp per match. Rate limited to 5 scans/hour per key.
Inventory stats returns total claims, unique hashes, date range, and monthly breakdown — scoped to your organization.

Census — Derived Lists

Create opaque HMAC-SHA256 derived lists for third-party hash verification without revealing your organization's hash inventory. Lists are signed by CertiSigma's ECDSA key for integrity verification. Requires census scope and org_id on the API key.

python
import hashlib, hmac

# 1. Create a derived list from explicit hashes
dl = client.create_derived_list(
    hashes=["a665a459...", "b4c9a289..."],
    label="Partner audit Q1",
    expires_in_hours=720,
)
print(f"List ID: {dl.id}")
print(f"List key (save now!): {dl.list_key}")

# 2. Or create from tag filter
dl2 = client.create_derived_list(
    tag_filter={"and": [{"key": "department", "value": "hr"}]},
    label="HR confidential subset",
)

# 3. Third party: match files against the list
# (can be done WITHOUT an API key — only needs list_key)
from certisigma import CertiSigmaClient
public = CertiSigmaClient()

file_hash = "a665a459..."
derived = hmac.new(
    bytes.fromhex(dl.list_key),
    file_hash.encode(),
    hashlib.sha256,
).hexdigest()

result = public.match_derived_list(dl.id, dl.list_key, [derived])
print(f"Matched: {result.matched}/{result.total}")

# 4. Verify list signature (public)
sig = public.get_derived_list_signature(dl.id)
print(f"Signed by: {sig.signing_key_id}")

# 5. Owner: list, inspect, revoke
lists = client.list_derived_lists()
detail = client.get_derived_list(dl.id)
log = client.get_derived_list_access_log(dl.id)
client.revoke_derived_list(dl.id)
ConstraintValue
Max items per list100,000 (server-configured)
Max TTL2,160 hours (90 days, server-configured)
Max hashes per match50,000 (server-configured)
Match rate limit20 requests/hour per list (server-configured)
Match endpoint authPublic (requires list_key)
Signature endpoint authPublic
Required scopecensus
Required key configorg_id must be set
Cryptographic properties: Derived hashes are HMAC-SHA256 transformations — the third party cannot reverse-engineer the original hashes. Lists are ECDSA-signed by CertiSigma, verifiable via GET /keys. Lists can be time-limited and revoked at any time.

Read Metadata

Explicit metadata read without re-verifying the attestation:

python
meta = client.get_metadata("att_42")
print(f"Source: {meta.source}, Extra: {meta.extra_data}")

Requires metadata:read scope. Share tokens with the matching attestation ID can also call this endpoint.

Verification Examples

python
# 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}")

Public Verification (No API Key)

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.

python
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}")
Public vs authenticated verify: Without API key, verify returns only cryptographic proof (hash, timestamp, level, signature) — no source or extra_data. With API key, it also returns your claim's metadata when you own a claim on that hash.

verify, batch_verify, status, get_evidence, 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.

status() Response Shape

Returns the current trust tier of an attestation without the full evidence payload. Useful for dashboards, progress tracking, and lightweight polling:

FieldTypeDescription
idstrAttestation ID (att_...)
hash_hexstrSHA-256 hash
level"T0" | "T1" | "T2"Current trust tier
t0_timestampstrECDSA signature timestamp
t1_timestampstr?TSA / Merkle batch timestamp
t2_timestampstr?Bitcoin anchor timestamp
t2_bitcoin_blockint?Bitcoin block height
signature_availableboolT0 ECDSA signature present
merkle_proof_availableboolT1 Merkle proof present
ots_availableboolOTS proof downloadable

verify vs status vs get_evidence

MethodInputAuthReturnsBest for
verify(hash)SHA-256 hashOptionalExistence, level, timestamp, signatureCompliance checks, "does this hash exist?"
status(att_id)Attestation IDNoTrust tier, availability flags, timestampsDashboards, progress tracking, polling
get_evidence(att_id)Attestation IDNoFull T0+T1+T2 proofs, Merkle path, OTSIndependent verification, long-term archival
Need claim isolation rules? See Security & Privacy Model for the full public/private boundary. See the API Reference for exact HTTP semantics.

Rate Limits

The API enforces per-key rate limits using a sliding window:

LimitDefaultNotes
Requests / minute1,000Configurable per key by admin
Monthly quotaUnlimitedOptional, per plan
Batch max size100Hashes per batch call

When rate-limited, the API returns 429 Too Many Requests with these headers:

  • Retry-After: 60
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

The SDK automatically raises RateLimitError with a retry_after property.

python
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")

Public Endpoint Protection

Public endpoints (verify, status, get_evidence) do not require an API key and are not subject to per-key rate limits. They are protected at the infrastructure level:

LayerScopeLimit
ApplicationPer API key1,000 req/min (authenticated endpoints only)
InfrastructurePer client IPReverse proxy rate limiting on all endpoints (public and authenticated)

Abusive traffic (automated scraping, enumeration floods) is mitigated by infrastructure-level IP rate limiting. Legitimate verification traffic at normal volumes is never throttled.

Polling vs Webhooks

Attestations progress through levels asynchronously. Two patterns to track upgrades:

Polling (simple, low-volume)

python
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")

Webhooks (production, real-time)

Register a webhook via the API (POST /webhook/register) to get notified when an attestation reaches T1 or T2.

EventWhenPayload includes
t1_completeTSA timestamp obtainedattestation_id, hash_hex, tsa_timestamp
t2_completeBitcoin anchor confirmedattestation_id, hash_hex, bitcoin_block, confirmed_at

Delivery Headers

Each webhook delivery includes these HTTP headers:

HeaderDescription
X-CertiSigma-EventEvent type (t1_complete or t2_complete)
X-CertiSigma-DeliveryUnique numeric delivery ID — use for deduplication
X-CertiSigma-Signaturesha256=<hex> HMAC-SHA256 of the raw JSON body

Verify the X-CertiSigma-Signature header using the SDK helper (constant-time HMAC-SHA256):

python
from certisigma import verify_webhook_signature

is_valid = verify_webhook_signature(raw_body, signature_header, signing_secret)
# Uses hmac.compare_digest internally — timing-attack resistant
Retry policy: Failed deliveries are retried 3 times with exponential backoff (1 min, 5 min, 15 min). Each delivery has a 10-second timeout. Use list_webhook_deliveries(webhook_id) or GET /webhook/{id}/deliveries to inspect delivery history.

Delivery Semantics

  • At-least-once delivery: Your endpoint may receive the same event more than once (e.g. on network retries). Use attestation_id from the payload as an idempotency key to deduplicate.
  • Ordering: Deliveries are sent in best-effort chronological order but ordering is not guaranteed. Do not rely on delivery order for state transitions.
  • Anti-replay: Check the created_at timestamp in the payload and reject deliveries older than your tolerance window (e.g. 5 minutes). The delivery ID (X-CertiSigma-Delivery) can also be tracked to detect replays.

Consumer Reference Pattern

python
import time

seen_deliveries = set()  # In production, use Redis or a DB table
MAX_AGE_SECONDS = 300    # 5-minute anti-replay window

def handle_webhook(headers, body):
    delivery_id = headers["X-CertiSigma-Delivery"]
    if delivery_id in seen_deliveries:
        return 200  # Already processed — idempotent ACK

    created = body["created_at"]  # ISO 8601 from payload
    age = time.time() - parse_iso(created).timestamp()
    if age > MAX_AGE_SECONDS:
        return 200  # Stale — ACK to stop retries

    if not verify_webhook_signature(raw_body, headers["X-CertiSigma-Signature"], secret):
        return 401  # Bad signature

    process_event(body["attestation_id"], headers["X-CertiSigma-Event"])
    seen_deliveries.add(delivery_id)
    return 200
Event vs delivery identity: attestation_id + event type identifies the event (what happened). X-CertiSigma-Delivery identifies the delivery attempt (retries share the same delivery ID). Deduplicate on delivery ID to handle retries; use attestation ID for business logic.

Secret Rotation

To rotate a webhook signing secret: register a new webhook with the same URL and events, verify it receives deliveries, then delete the old webhook. In-flight retries for the old webhook continue using the original secret.

Best practice: Use webhooks as the primary notification channel and polling as a fallback for missed events.

Webhook Management

The SDK exposes 4 methods for managing webhook subscriptions and a helper for verifying delivery signatures. All methods require an API key with the webhook scope.

Register a Webhook

python
result = client.register_webhook(
    url="https://my-server.com/census-hook",
    events=["t1_complete", "t2_complete"],
    label="census-watch-prod",  # optional, for identification
)
# result.id            → "wh_abc123"
# result.signing_secret → "a1b2c3..." (shown once — store securely)
# result.label         → "census-watch-prod"

List Webhooks

python
result = client.list_webhooks()
for wh in result.webhooks:
    print(wh.id, wh.url, wh.label, wh.failure_count)

Delete a Webhook

python
result = client.delete_webhook("wh_abc123")
# result.deleted → True

List Delivery History

python
result = client.list_webhook_deliveries("wh_abc123")
for d in result.deliveries:
    print(d.event_type, d.status, d.response_code)

Verify Webhook Signature

python
from certisigma import verify_webhook_signature

# In your webhook handler (Flask, FastAPI, etc.):
is_valid = verify_webhook_signature(
    payload=request.get_data(),       # raw bytes
    signature=request.headers["X-CertiSigma-Signature"],
    secret=signing_secret,            # from register_webhook().signing_secret
)
ConstraintValue
Eventst1_complete, t2_complete
Signing secret64-char hex, shown once at registration — store securely
URL schemeHTTPS required in production; HTTP allowed in demo mode
LabelOptional, max 200 characters
Delivery historyLast 100 deliveries per webhook

Evidence & OTS Verification

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.

Recommended Workflow

  1. Register a t2_complete webhook (or poll with status())
  2. When notified, call get_evidence(att_id)
  3. Save the updated manifest including T2 data
  4. Optionally save the .ots proof file for independent verification
python
from certisigma import CertiSigmaClient, get_blockchain_url, save_ots_proof
import json

client = CertiSigmaClient(api_key=os.environ.get("CERTISIGMA_API_KEY"))

# 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)

Evidence Response Fields

FieldTypeDescription
hash_hexstrSHA-256 hash of the attested data
levelstrHighest confirmed level: T0, T1, or T2
t0 — ECDSA Signature (immediate)
t0.signaturestrBase64-encoded ECDSA P-256 signature
t0.algorithmstrECDSA-P256-SHA256
t0.publicKeyIdstrSigning key identifier for key pinning
t0.registeredAtstrISO 8601 timestamp of registration
t1 — TSA Timestamp + Merkle Proof (minutes)
t1.batchIdintT1 batch identifier
t1.merkleRootstrMerkle root hash of the batch
t1.merkleProoflistMerkle inclusion proof path (leaf → root)
t1.tsaProviderstrTSA provider (e.g. sectigo_qualified)
t1.tsaTimestampstrRFC 3161 timestamp (ISO 8601)
t1.tsaTokenstrBase64 TSA token for independent verification
t2 — Bitcoin Anchor via OpenTimestamps (hours)
t2.dailyRootstrDaily Merkle root anchored to Bitcoin
t2.t1ToT2ProoflistMerkle proof linking T1 batch root to T2 daily root
t2.bitcoinBlockHeightintBitcoin block number containing the anchor
t2.bitcoinBlockHashstrBitcoin block hash
t2.bitcoinTxidstrBitcoin transaction ID
t2.confirmedAtstrISO 8601 confirmation timestamp
t2.otsProofstrRaw OpenTimestamps proof (Base64-encoded binary)

SDK Helper Functions

FunctionInputReturns
get_blockchain_url(evidence)EvidenceResultmempool.space block URL, or None
get_blockchain_url(evidence, type="tx")EvidenceResultmempool.space transaction URL, or None
save_ots_proof(evidence, path)EvidenceResult, strTrue if saved, False if T2 not ready

Independent OTS Verification

The saved .ots file is a standard OpenTimestamps proof. Verify it independently — no CertiSigma involvement needed:

bash
# 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
Full chain of trust: Your hash → T0 ECDSA signature → T1 Merkle tree + TSA timestamp → T2 Bitcoin blockchain anchor. Each tier is independently verifiable. The .ots proof ties directly to a Bitcoin block — immutable, decentralized, permanent.
Blockchain explorer: Use 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.
Need verification details? See Test Vectors for canonical T0/T1/T2 verification examples with real production data. See Security & Privacy Model for the full public/private data boundary.

Error Handling

python
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}")

Error Envelope

All API errors return a consistent JSON envelope:

json
{ "error": true, "code": "ERROR_CODE", "message": "Human-readable description", "details": null }

Error Codes & Retry Guidance

HTTPCodeRetryableAction
400 / 422VALIDATION_ERRORNoFix request payload
401AUTH_ERRORNoCheck API key
403FORBIDDENNoInsufficient permissions or scope
404NOT_FOUNDNoResource does not exist
429RATE_LIMITYesBack off using Retry-After header
500INTERNAL_ERRORYesRetry with exponential backoff
Retry strategy: Only retry on 429 and 5xx. Fail fast on all other 4xx errors — they indicate a client-side issue that retrying will not resolve. The SDK exposes e.status_code and e.retry_after (on RateLimitError) for implementing backoff logic.

Example Error Responses

json — 429 Rate Limit
{ "error": true, "code": "RATE_LIMIT", "message": "Rate limit exceeded", "details": null }
// Headers: Retry-After: 12, X-RateLimit-Remaining: 0
json — 401 Auth Error
{ "error": true, "code": "AUTH_ERROR", "message": "Invalid or missing API key", "details": null }
json — 422 Validation Error
{ "error": true, "code": "VALIDATION_ERROR", "message": "hash_hex must be exactly 64 lowercase hex characters", "details": null }
json — 500 Internal Error
{ "error": true, "code": "INTERNAL_ERROR", "message": "An internal error occurred", "details": null }
Request tracing: Every API response includes an X-Request-Id header. You can send your own X-Request-Id in the request — the API echoes it back. If omitted, the server generates one automatically. Use it for technical support/debugging — include it when contacting support. For domain audit, use attestation_id — it identifies the attested record, not the HTTP request.

Security Best Practices

  • Environment variables: Store API keys in CERTISIGMA_API_KEY, never in code.
  • Client-side encryption: Use encrypt_metadata() for sensitive data in extra_data.
  • Key rotation: Use rotate_key(envelope, old_key, new_key) to re-encrypt without decrypting on the server.
  • Data isolation: Attestation IDs are sequential and public by design. Public endpoints (verify, get_evidence, status) return only cryptographic proof (hash, signature, Merkle, OTS). Organizational metadata (source, extra_data) is never exposed publicly — only the authenticated API key owner can read their own claim data.
  • IP Allowlist: Restrict API key usage to known IP ranges.
  • Webhook secrets: Store the signing_secret in a secrets manager. It is shown only once.
  • Audit trail: Log all attestation IDs and hashes locally for independent auditing.

When to Use What

Use caseRecommended approach
One-off file attestationattest_file(path)
Serial number / identifier attestationattest_string(text) — hash client-side, no temp file
Bulk ingestion pipelinehash_file() + batch_attest()
Audit / compliance checkverify() or batch_verify() (no API key needed)
Verify a specific file or stringverify_file(path) or verify_string(text)
Leak detection (exfiltration)scan(suspect_hashes) — compare against org inventory
Inventory dashboardget_bulk_stats() — total claims, monthly breakdown
Long-term proof exportget_evidence() + save_ots_proof()
Sensitive metadataClient-side encryption with encrypt_metadata()
High-volume async notificationsWebhooks (t1_complete, t2_complete), not polling
Blockchain verification URLget_blockchain_url(evidence)

T0 Signature Format

Every attestation receives an immediate ECDSA-P256-SHA256 signature (T0). To verify independently:

Payload componentEvidence JSON fieldInvariant
format_versiont0.formatVersionSemver string, currently 2.0.0
hash_hexhash_hexLowercase hex, exactly 64 characters (SHA-256)
registered_att0.registeredAtISO 8601, microsecond precision, UTC offset +00:00
(e.g. 2026-03-16T12:00:00.000000+00:00)
Naming convention: The payload uses snake_case labels (format_version, registered_at). The evidence JSON response uses camelCase (t0.formatVersion, t0.registeredAt). When reconstructing the payload for verification, read the camelCase fields from the evidence and place them into the pipe-separated payload.

Payload construction: {format_version}|{hash_hex}|{registered_at} — pipe U+007C, no whitespace, UTF-8 encoded.

Signing: SHA-256 of the UTF-8 payload bytes, then ECDSA P-256 over the hash (prehashed). Signature is base64-encoded DER.

Independent Verification Steps

  1. Retrieve public keys from GET /keys.
  2. Match the key using t0.publicKeyId from your evidence.
  3. Reconstruct payload: f"{evidence.t0['formatVersion']}|{evidence.hash_hex}|{evidence.t0['registeredAt']}"
  4. Compute SHA-256 of the UTF-8 payload bytes.
  5. Verify the ECDSA-P256-SHA256 signature (t0.signature, base64-decoded DER) using the JWK public key.

Server-Side Key Rotation

GET /keys returns all active signing keys. Old attestations reference the key that was active at signing time. Always match via t0.publicKeyId; never assume a single key.

Test Vectors

Canonical test vectors from a production attestation (att_500). Use these to validate any independent verification implementation against real CertiSigma output.

T0 — ECDSA Signature Verification

FieldValue
hash_hex7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac
t0.formatVersion2.0.0
t0.registeredAt2026-02-28T00:33:19.291619+00:00
t0.publicKeyIdcertisigma-3cb3ab64f5cfc983
t0.signatureMEQCIGuKNpN1MERfn7jeJW4aRG2om7nIp2eRz/GHueM/Hi9qAiA2ik/6not2lJUWnYGpQ6x9xouOt8j8Cr42vBVSrSeN+Q==

Step 1 — Reconstruct canonical payload

text — canonical payload (103 bytes UTF-8)
2.0.0|7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac|2026-02-28T00:33:19.291619+00:00

Step 2 — SHA-256 of canonical payload

text — expected SHA-256
f5203ed4089a3bd3367f9301de43db93b574e92e1d597227c738d5338cd5ae01

Step 3 — Public key (JWK from GET /keys)

json — P-256 public key
{
  "kty": "EC", "crv": "P-256", "alg": "ES256",
  "kid": "certisigma-3cb3ab64f5cfc983",
  "x": "7AKMIyF5Rm3SfCASYsJsBHvZhF9cqtjEcmbu5DL8wA8",
  "y": "94l-xPf8HVkp03XELgIeDvsBDtWTL8tj7glvfjFU1Fk"
}

Step 4 — Verify ECDSA-P256-SHA256

python — full verification
import hashlib, base64
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives import hashes

# Evidence fields
hash_hex     = "7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac"
version      = "2.0.0"
registered   = "2026-02-28T00:33:19.291619+00:00"
sig_b64      = "MEQCIGuKNpN1MERfn7jeJW4aRG2om7nIp2eRz/GHueM/Hi9qAiA2ik/6not2lJUWnYGpQ6x9xouOt8j8Cr42vBVSrSeN+Q=="

# Public key (base64url → bytes)
def b64url(s):
    return base64.urlsafe_b64decode(s + "==")

x = b64url("7AKMIyF5Rm3SfCASYsJsBHvZhF9cqtjEcmbu5DL8wA8")
y = b64url("94l-xPf8HVkp03XELgIeDvsBDtWTL8tj7glvfjFU1Fk")
pub = ec.EllipticCurvePublicNumbers(
    int.from_bytes(x, "big"),
    int.from_bytes(y, "big"),
    ec.SECP256R1()
).public_key()

# Reconstruct + hash + verify
payload = f"{version}|{hash_hex}|{registered}".encode("utf-8")
digest  = hashlib.sha256(payload).digest()
sig_der = base64.b64decode(sig_b64)

pub.verify(sig_der, digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())))
# Alternative without Prehashed: pass payload directly, let the library hash
# pub.verify(sig_der, payload, ec.ECDSA(hashes.SHA256()))
print("✅ T0 signature verified")
Prehashed vs standard ECDSA: CertiSigma signs the SHA-256 digest using Prehashed. Most crypto libraries hash internally when verifying — in that case, pass the raw payload (not the digest) and let the library compute SHA-256. If you pass a pre-computed digest to a library that hashes internally, you get a double-hash and verification fails. Python cryptography: use Prehashed(SHA256()) with digest, or omit it and pass payload. WebCrypto / Node.js subtle.verify: always pass payload (WebCrypto has no prehash mode).

T1 — Merkle Inclusion Proof (RFC 6962 v2)

FieldValue
Leaf hash7fd9101e...cf0aac (same as hash_hex)
Merkle root1ccb4cfd5ea666d2f7418f104cebf200ef258b784dc483698335a084cf0f1d67
Merkle version2 (RFC 6962 domain separation: H(0x01 || left || right))
Proof path3 steps (see below)
text — proof traversal
Leaf: 7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac

Step 0: H(0x01 || 63792cf5...b8e026 || 7fd9101e...cf0aac) → 844370b7ddafe4f9...
Step 1: H(0x01 || 82950bd8...631955 || 844370b7...e4f9..) → 4529ddc8d19457a9...
Step 2: H(0x01 || 4529ddc8...57a9.. || a35f3d52...27f35e) → 1ccb4cfd5ea666d2...

Computed root: 1ccb4cfd5ea666d2f7418f104cebf200ef258b784dc483698335a084cf0f1d67 ✅
python — Merkle proof verification
import hashlib

leaf = "7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac"
root = "1ccb4cfd5ea666d2f7418f104cebf200ef258b784dc483698335a084cf0f1d67"
proof = [
    {"hash": "63792cf512a74247fe1ac05e25494f40143fe0f6da9b1db855e9444035b8e026", "position": "left"},
    {"hash": "82950bd8a3b929aaf12905e2457c91240c9f479a64888a1bd53fbdee23631955", "position": "left"},
    {"hash": "a35f3d526cbffff89d8099580271cb855109e84d357ed4cf31a46f7d0427f35e", "position": "right"},
]

current = bytes.fromhex(leaf)
for step in proof:
    sibling = bytes.fromhex(step["hash"])
    if step["position"] == "left":
        current = hashlib.sha256(b"\x01" + sibling + current).digest()
    else:
        current = hashlib.sha256(b"\x01" + current + sibling).digest()

assert current.hex() == root, "Merkle proof failed"
print("✅ Merkle inclusion verified")

T2 — Bitcoin Anchor

FieldValue
Daily rootb7e7f6bfcd2419864d5021df4a0c9345faa48b9a4de0e1f45ee6158ffb5e02e0
Bitcoin block938792
Confirmed at2026-03-01T03:53:18.079007+00:00
OTS proofBinary proof available via GET /attestation/att_500/ots/proof
T2 independent verification: Download the .ots proof file and verify with any OpenTimestamps-compatible tool (e.g. ots verify att_500.ots). The proof chains from the daily Merkle root to a Bitcoin block header, providing mathematical proof of existence before that block's timestamp.

Negative Test Vector (must fail)

Same attestation with registeredAt altered by one microsecond (291619291620). Any correct implementation must reject this signature:

python — expected failure
altered = "2.0.0|7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac|2026-02-28T00:33:19.291620+00:00"
try:
    bad_digest = hashlib.sha256(altered.encode()).digest()
    pub.verify(sig_der, bad_digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())))
    print("❌ BUG: should have failed")
except:
    print("✅ Correctly rejected altered timestamp")

Machine-Readable Test Vectors

All test vectors are available as a single JSON file for automated testing:

GET /test-vectors.json — contains T0 (positive + negative), T1, and T2 data with all raw values copy-safe.

Need exact HTTP semantics? See the API Reference for full OpenAPI specification, request/response schemas, and error codes.

Security & Privacy Model

CertiSigma is a public attestation platform. Cryptographic proofs are intentionally public; organizational metadata is strictly private. This section formalizes the boundary.

CategoryVisibilityDetails
Attestation IDsPublicSequential (att_1, att_2, …). Discoverable by design — any party can look up any ID.
SHA-256 hashPublicThe attested hash is visible on public endpoints. It does not reveal file contents (preimage resistance).
ECDSA signature (T0)PublicVerifiable by anyone via GET /keys.
Merkle proof (T1)PublicInclusion proof is publicly verifiable.
OTS / Bitcoin anchor (T2)PublicDownloadable .ots proof, verifiable against Bitcoin blockchain.
TimestampsPublicregisteredAt, t1_timestamp, t2_timestamp are always visible.
sourcePrivateOnly returned to the authenticated API key owner who created the claim.
extra_dataPrivateOnly returned to the authenticated API key owner. Use encrypt_metadata() for defense in depth.
TagsPrivateScoped per API key. Other keys cannot see or query your tags, even on the same attestation. Supports client-side encryption for sensitive values.
Claim existencePrivatePublic endpoints show the attestation exists, not which organizations hold claims on it.

Threat Model

The sequential nature of attestation IDs means an external party can enumerate all attestation IDs and retrieve their public cryptographic proofs. This is accepted by design because:

  • CertiSigma is an attestation platform — public verifiability is the core value proposition.
  • Public endpoints return only cryptographic proof (hash, signature, Merkle, OTS). No organizational metadata is ever exposed.
  • The SHA-256 hash is a one-way function — knowing the hash does not reveal the attested content.
  • For sensitive metadata, client-side encryption (AES-256-GCM) ensures the server itself never sees plaintext.

What the Server Sees

DataServer has access?
SHA-256 hashYes — required for attestation
Original file / contentNever — only the hash is sent
source (optional label)Yes, if provided — plaintext string, not encryptable. Do not include PII.
extra_data (plaintext)Only if the client chooses not to encrypt. The SDK supports AES-256-GCM client-side encryption — use it for any sensitive metadata.
extra_data (client-encrypted)Never — server stores only the AES-256-GCM ciphertext, cannot decrypt
API key identityYes — required for authenticated endpoints
Client IP (general API)Seen at transport level — used for infrastructure rate limiting. Not stored in any application-level database table for normal API operations (attest, verify, metadata, tags).
Client IP (forensic sharing)When a third party accesses data via share token or performs a derived list match, the IP is recorded in the access log. This log is visible only to the data owner (the API key holder who created the token/list). This enables forensic audit of who accessed shared data.
Recommendation: If your extra_data contains PII or business-sensitive information, always use client-side encryption. This provides defense in depth: even if the server were compromised, the attacker would only see ciphertext. source is always plaintext but optional and never publicly visible — only you (the authenticated API key owner) can read it. If in doubt, omit it.

Configuration Reference

ParameterDefaultDescription
api_keyNoneBearer token (cs_live_... or cs_demo_...). Optional for verify/health.
base_urlhttps://api.certisigma.chAPI endpoint
timeout30.0Request timeout in seconds

Requirements

  • Python 3.10+
  • httpx >= 0.25.0
  • cryptography >= 44.0.0 (optional, for certisigma.crypto)

Advanced Configuration

TopicDetails
Proxyhttpx respects HTTP_PROXY / HTTPS_PROXY / NO_PROXY environment variables automatically.
Custom TLS CASystem CA bundle by default. For custom CAs (e.g. corporate proxy), configure httpx with verify="/path/to/ca-bundle.pem".
RetryNot built-in. Implement at caller level with exponential backoff on 429 / 5xx (see Error Handling above).
ObservabilityNo built-in hooks. Use httpx event hooks for request/response logging. Redact the Authorization header in logs.

Resilient Client Wrapper

Reference implementation with retry, backoff, and logging:

python
import time, logging, os
from certisigma import CertiSigmaClient, CertiSigmaError, RateLimitError

log = logging.getLogger("certisigma")

def resilient_call(fn, *args, retries=3, **kwargs):
    """Retry on 429/5xx with exponential backoff and jitter."""
    for attempt in range(retries):
        try:
            result = fn(*args, **kwargs)
            log.info("OK %s attempt=%d", fn.__name__, attempt + 1)
            return result
        except RateLimitError as e:
            wait = e.retry_after or 2 ** attempt
            log.warning("429 retry_after=%s attempt=%d", wait, attempt + 1)
            time.sleep(wait)
        except CertiSigmaError as e:
            if e.status_code and e.status_code >= 500 and attempt < retries - 1:
                wait = 2 ** attempt
                log.warning("5xx status=%s attempt=%d", e.status_code, attempt + 1)
                time.sleep(wait)
            else:
                raise
    raise RuntimeError("Max retries exceeded")

client = CertiSigmaClient(api_key=os.environ["CERTISIGMA_API_KEY"])
result = resilient_call(client.attest, hash_hex, source="pipeline")

Sandbox vs Production

AspectSandbox (cs_demo_...)Production (cs_live_...)
ECDSA signingEphemeral key (may rotate on restart)Persistent key pair
T1 (TSA)Free TSA providerQualified TSA
T2 (Bitcoin)Same anchoring pipelineSame anchoring pipeline
Webhook URLshttp:// allowedhttps:// required
Rate limit1,000/min (default)1,000/min (configurable per key/org)
Data retentionPersistent (same DB)Persistent
OpenAPI explorerAvailable at /docsDisabled

To promote an integration from sandbox to production, replace the api_key with a cs_live_... key. No code changes required.

Package

PyPI   certisigma  —  MIT License — Ten Sigma Sagl

Compatibility Policy

  • Semver: This SDK follows Semantic Versioning 2.0.0. Patch releases contain only bug fixes; minor releases add features without breaking changes; major releases may contain breaking changes.
  • API stability: The SDK targets API /v1/. When a new API version is introduced (e.g. /v2/), the previous version will remain supported for a minimum of 12 months.
  • SDK–API mapping: SDK v1.x targets API v1. New API features are supported in minor SDK releases.
  • Support window: The latest minor release receives bug fixes and patches. The previous minor release receives critical security patches for 6 months after the next minor release.
  • Deprecation: Deprecated features are marked in the changelog and emit warnings for at least one minor version before removal.

Changelog

v1.10.0

  • New: hash_string() — SHA-256 of UTF-8 strings (no temp file needed)
  • New: attest_string() — hash + attest in one call (source never auto-populated)
  • New: verify_string() — hash + verify for strings
  • New: verify_file() — hash + verify for files (symmetry with attest_file())
  • Cross-SDK determinism: Python and JavaScript produce identical hashes for the same string input

v1.6.0

  • New: create_share_token(), list_share_tokens(), get_share_token_info(), revoke_share_token() — forensic metadata sharing with time-limited, auditable tokens
  • New: put_tags(), get_tags(), delete_tag(), query_tags() — structured multi-dimensional tagging with server-side AND queries (max 10 conditions)
  • New: get_metadata() — explicit metadata read without re-verifying
  • New: RBAC scoped keys — metadata:read, metadata:write, tags:read, tags:write, share
  • New: batch_verify(detailed=True) — returns full claim metadata in batch mode (requires API key)

v1.5.0

  • New: request ID header (X-Request-ID) included in all requests

v1.4.0

  • New: get_blockchain_url(evidence) — returns a mempool.space URL for the Bitcoin block or transaction
  • New: save_ots_proof(evidence, path) — saves the raw OpenTimestamps .ots proof to a local file

v1.3.0

  • New: api_key is now optional — public endpoints (verify, batch_verify, health) work without authentication
  • New: AuthenticationError raised immediately if auth-required method called without key (no request sent)

v1.2.0

  • New: standalone hash_file() and hash_bytes() utility functions
  • Fix: PyPI project URL now links to SDK documentation

v1.1.1 — Initial Public Release

  • Full API coverage: attest, verify, batch, file attestation, metadata, evidence, status, health
  • Sync (CertiSigmaClient) and async (AsyncCertiSigmaClient) clients
  • Client-side AES-256-GCM encryption with key rotation support
  • Typed error hierarchy: AuthenticationError, RateLimitError, QuotaExceededError
  • Full type annotations (py.typed marker)

How It Works

CertiSigma provides three-tier cryptographic attestation for any SHA-256 hash:

  1. T0 — ECDSA Signature — Immediate. The server signs your hash with a P-256 key upon receipt. POST /attest
  2. T1 — TSA Timestamp — Within minutes. Hashes are batched into a Merkle tree and submitted to an RFC 3161 Time Stamping Authority. POST /verify
  3. T2 — Bitcoin Anchor — Within hours. The Merkle root is anchored to the Bitcoin blockchain via OpenTimestamps, providing immutable proof. GET /attestation/{id}/evidence

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.

Installation

Zero dependencies. Works in Node.js 18+ and modern browsers (native fetch).

bash
npm install @certisigma/sdk

Full TypeScript definitions included (src/index.d.ts, src/crypto.d.ts).

Runtime & Packaging

Node.js 18+ (native fetch, Web Crypto API). Subpath exports: @certisigma/sdk (main), @certisigma/sdk/hash, @certisigma/sdk/crypto. CommonJS (require) and ESM (import) supported. Edge runtimes (Vercel Edge, Cloudflare Workers, Deno, Bun): use native fetch; no polyfills required. For large files in Node, prefer streaming hashing or Python's attest_file(path).

Authentication

Pass your API key to the client constructor. The SDK sends it as a Bearer token on every request.

javascript
const { CertiSigmaClient } = require('@certisigma/sdk');

const client = new CertiSigmaClient({
  apiKey: process.env.CERTISIGMA_API_KEY,
});
Key formats: 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.

Quick Start

javascript
(async function main() {
  const { CertiSigmaClient, hashFile, hashBytes } = require('@certisigma/sdk');
  const fs = require('fs');

  const client = new CertiSigmaClient({ apiKey: process.env.CERTISIGMA_API_KEY });

  // 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);
})();

Integration Contract

ParameterValue
API target/v1/ (all endpoints also available without prefix)
Rate limit1,000 req/min per key (sliding window), Retry-After header on 429
Batch max100 hashes per call (batchAttest, batchVerify)
Public endpointsverify, batchVerify, status, getEvidence, health, /keys, matchDerivedList (requires listKey), getDerivedListSignaturebatchVerify({ detailed: true }) requires apiKey
Auth-requiredattest, batchAttest, updateMetadata, deleteMetadata, getMetadata, putTags, getTags, queryTags, createShareToken, scan, getBulkStats, createDerivedList, listDerivedLists, getDerivedList, getDerivedListAccessLog, revokeDerivedList, registerWebhook, listWebhooks, deleteWebhook, listWebhookDeliveries
RBAC scopesattest, metadata:read, metadata:write, tags:read, tags:write, share, scan, batch, census, webhook — see RBAC & Scoped Keys
Attestation IDsSequential (att_1, att_2, …). Public by design — CertiSigma is a public attestation platform. Cryptographic proof (hash, signature, Merkle, OTS) is intentionally verifiable by any party.
Data isolationOrganizational metadata (source, extraData, tags) is never exposed on public endpoints. Only the authenticated API key owner can read their own claim data. Public endpoints return only cryptographic proof.
Retry policyCaller-managed. Retry on 429 and 5xx with exponential backoff + jitter
IdempotencyRe-attesting the same hash returns the existing attestation (no duplicate)
Webhook deliveryAt-least-once, ordering not guaranteed, HMAC-SHA256 signed
Support windowCurrent major + previous major for at least 12 months
DeprecationAt least one minor release warning before removal

Recommended Attestation Workflow

A production integration typically follows this pattern:

javascript
(async function run() {
  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; loads full file into memory)
  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));
})();

Attest Response Fields

FieldTypeDescription
idstringAttestation ID (att_...)
hash_hexstringSHA-256 hash that was attested
timestampstringISO 8601 creation time
signaturestringBase64 ECDSA P-256 signature
statusstringcreated (new) or existing (already attested)
etagstringETag for optimistic concurrency on metadata updates
claim_idnumberYour API key's claim ID for this attestation
sourcestringSource label you provided
extra_dataobjectMetadata you attached

verify() Response Shape

FieldJS typePublic (no key)Authenticated
existsbooleanAlwaysAlways
hash_hexstringAlwaysAlways
idstring?If existsIf exists
timestampstring?If existsIf exists
levelstring?If existsIf exists
verified_atstringIf existsIf exists
sourcestring?If caller owns a claim

getEvidence() Response Shape

FieldJS typeCondition
idstringAlways
hash_hexstringAlways
levelstringAlways (T0, T1, or T2)
t0objectAlways (keys: formatVersion, registeredAt, signature, publicKeyId)
t1object?Present when level ≥ T1 (keys: tsa, timestamp, token)
t2object?Present when level = T2 (keys: merkleRoot, bitcoinBlock, txId)
Naming: Both Python and JS SDKs use snake_case for top-level response fields (hash_hex, extra_data, verified_at). Nested objects under t0, t1, t2 use camelCase (formatVersion, registeredAt, publicKeyId).

What to Save After Attestation

Always persist the attestation result locally. Example JSON manifest pattern:

json
{
  "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"
  }
}
Local manifest: Save a .certisigma.json file alongside each attested file. This creates an offline-verifiable record without querying the API.

Minimum Durable Evidence Set (Probative Use)

For long-term legal or audit purposes, persist at least: id, hash_hex, timestamp, signature, level, and when available t1 (TSA + Merkle proof) and t2 (Bitcoin anchor). Save the .ots proof file for independent verification. This set allows verification even if the CertiSigma API is unavailable.

Idempotency

Attest returns status: "created" for new attestations or status: "existing" if the hash was already attested. Safe to retry: duplicate attestations are idempotent. On network timeout, re-attest with the same hash; if it already exists, you get the same attestation ID.

Batch Operations

Attest or verify up to 100 hashes in a single API call.

javascript
(async function run() {
  const { hashFile } = require('@certisigma/sdk');
  const fs = require('fs');

  // Compute hashes for all invoices (each file loaded fully into memory)
  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 — returns full claim metadata per item
  const batch = await client.batchAttest(hashes, { source: 'invoice-processor' });
  console.log(`Created: ${batch.created}, Existing: ${batch.existing}`);
  batch.attestations.forEach(a =>
    console.log(`  ${a.id} claim=${a.claim_id} src=${a.source}`)
  );

  // 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}`);

  // Detailed mode — certification level + claim metadata (requires apiKey)
  const detailed = await client.batchVerify(currentHashes, { detailed: true });
  detailed.results.filter(r => r.exists).forEach(r =>
    console.log(`  ${r.id} level=${r.level} src=${r.source}`)
  );
})();

Batch attest now returns full claim metadata per item (claim_id, source, extra_data, signing_key_id, etag).
Batch verify supports { detailed: true } for enriched results including certification level and tier timestamps. detailed mode requires apiKey; without a key the API returns 401.

File Attestation

Hash a file client-side (via Web Crypto) and attest the SHA-256 in one call. Works with File, Blob, ArrayBuffer, or Uint8Array.

Memory usage (JS): hashFile() and attestFile() load the entire file into memory (unlike Python's streaming hash_file()). For large files, prefer Python's attest_file(path) or stream-based hashing in Node.
javascript
// Browser — from a file input (inside an async function)
const file = document.getElementById('fileInput').files[0];
const result = await client.attestFile(file, { source: 'upload-form' });

// Node.js — from a buffer (inside an async function)
const buf = fs.readFileSync('document.pdf');
const result2 = await client.attestFile(buf, { source: 'cli-tool' });

Hashing Utilities

Standalone SHA-256 functions to compute hashes without attestation. Available from the main package or @certisigma/sdk/hash.

javascript
(async function run() {
  const { hashFile, hashBytes, hashString } = require('@certisigma/sdk');
  // or: const { hashFile, hashBytes, hashString } = require('@certisigma/sdk/hash');

  // Hash a File, Blob, ArrayBuffer, or Uint8Array
  const fileHash = await hashFile(fileInput.files[0]);

  // Hash raw bytes
  const dataHash = await hashBytes(new TextEncoder().encode('raw content'));

  // Hash a string (UTF-8, no file needed)
  const strHash = await hashString('SN-2026-001234');

  // 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' });
})();
FunctionInputReturns
hashFile(input)File | Blob | ArrayBuffer | Uint8ArrayPromise<string> — 64-char hex
hashBytes(data)Uint8Array | ArrayBufferPromise<string> — 64-char hex
hashString(text)stringPromise<string> — 64-char hex

String Attestation

Attest identifiers, serial numbers, or any non-file content without creating temporary files. The string is hashed client-side (UTF-8 via TextEncoder); only the SHA-256 hash reaches the API.

Canonical form: UTF-8, no BOM, no trailing newline, no Unicode normalization. Cross-SDK determinism is guaranteed: Python and JavaScript produce identical hashes for the same input.
javascript
// One-step: hash + attest
const result = await client.attestString('SN-2026-001234', { source: 'serial-registry' });

// Verify later
const check = await client.verifyString('SN-2026-001234');
console.assert(check.exists);

// Also works for files
const fileCheck = await client.verifyFile(fileInput.files[0]);
Security note: source is never auto-populated from the string content (unlike attestFile which uses the filename). Pass source explicitly if needed.
MethodAuth RequiredDescription
attestString(text, { source, extraData })YesHash string + create attestation
verifyString(text)NoHash string + verify attestation
verifyFile(file)NoHash file + verify attestation

Metadata and Claims

Each API key owns its own claim on an attestation. Claims hold source (an optional label) and extraData (a JSON object). Multiple API keys can independently claim the same hash.

source is an optional plaintext operational label (max 100 chars) — designed for system identifiers like pipeline names or app versions, not for sensitive or personal data. If you don't need it, simply omit it. It is never exposed on public endpoints; only you (the authenticated API key owner) can read it. For sensitive metadata, use extraData with client-side encryption.

javascript
(async function run() {
  // 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');
})();
Optimistic concurrency: The API returns an 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.

Client-Side Encryption (Zero Knowledge)

Encrypt metadata with AES-256-GCM before sending. When clientEncrypted=true, the server stores extraData as an opaque blob and never sees plaintext. Uses the Web Crypto API.

What about source? source is always plaintext by design — it's an optional operational label that the backend uses for audit logging and filtering. Encrypting it would make it unusable for those purposes. It is never exposed on public endpoints; only you (the authenticated claim owner) can read it. If you don't need it, omit it entirely. Never store PII in source — use encrypted extraData instead.
javascript
(async function run() {
  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);
  const result = await client.attest(hash, { extraData: encrypted, clientEncrypted: true });

  // Decrypt after retrieving (extraData comes from the attest response, not verify)
  const plaintext = await decryptMetadata(result.extra_data, key);
})();

Available crypto functions: generateKey(), encryptMetadata(), decryptMetadata(), isEncrypted(), rotateKey().

Key management: Store encryption keys in a dedicated secrets manager (Vault, AWS KMS, etc.). If you lose the key, the encrypted data is irrecoverable — the server cannot help.

RBAC & Scoped API Keys

API keys carry granular scopes that control access to specific operations. Keys created before RBAC was introduced are automatically backfilled with all scopes for backward compatibility.

ScopeGrants
attestCreate attestations (attest, batchAttest)
batchBatch operations + inventory stats (batchAttest, batchVerify, getBulkStats)
metadata:readRead claim metadata (getMetadata)
metadata:writeUpdate/delete metadata (implies metadata:read)
tags:readRead tags (getTags)
tags:writeCreate/update/delete tags (implies tags:read)
shareCreate and manage share tokens
scanLeak detection (scan) — requires org_id
censusDerived lists (createDerivedList, etc.) — requires org_id
webhookRegister, list, and delete webhooks (registerWebhook, listWebhooks, deleteWebhook, listWebhookDeliveries)

verify, status, getEvidence, and health are public and require no scope (or no API key at all).

Scope inheritance: metadata:write automatically grants metadata:read; tags:write automatically grants tags:read. You do not need to request both.

Share Tokens (Forensic Metadata Sharing)

Create time-limited, auditable, revocable tokens for sharing attestation metadata with forensic analysts, auditors, or third-party verifiers. Share tokens grant read-only access to a specific set of attestations.

javascript
(async function run() {
  const token = await client.createShareToken([42, 43, 44], {
    expiresIn: 86400,             // 24 hours (max 30 days)
    recipientLabel: 'forensic-analyst',
    maxUses: 10,
  });
  console.log(`Share token: ${token.share_token}`); // shown once, save it

  // List, inspect, revoke
  const tokens = await client.listShareTokens();
  const info = await client.getShareTokenInfo(token.id);
  await client.revokeShareToken(token.id);
})();
ConstraintValue
Max expiry30 days
Max attestation IDs100 per token
Max usesOptional (unlimited if unset)
Rate limit100 req/min (separate from API key rate limit)
Required scopeshare
Security: Share tokens are stored as SHA-256 hashes — the plaintext token is shown only once at creation. They grant metadata:read and tags:read only for the specified attestation IDs. All access is logged in the share access log.

Structured Tagging

Classify attestations with multi-dimensional key-value tags. Tags support server-side querying (AND semantics) and optional client-side encryption for sensitive values.

javascript
(async function run() {
  // Upsert tags (max 50 per attestation per key)
  await client.putTags('att_42', [
    { key: 'department', value: 'hr' },
    { key: 'classification', value: 'confidential' },
  ]);

  const tags = await client.getTags('att_42');

  // Query by tags (AND semantics, max 10 conditions)
  const results = await client.queryTags(
    { and: [
      { key: 'department', value: 'hr' },
      { key: 'classification', value: 'confidential' },
    ]},
    { limit: 100 },
  );
  console.log(`Found ${results.count} matching attestations`);

  // Delete a tag
  await client.deleteTag('att_42', 'classification');
})();
ConstraintValue
Max tags per attestation per key50
Tag key format^[a-z][a-z0-9_-]{0,62}$ (lowercase, no _ prefix — reserved)
Max AND conditions per query10
Client-encrypted tagsSupported (valueEnc + valueNonce hex), but excluded from server-side query
Required scope (read)tags:read
Required scope (write)tags:write
Tenant isolation: Tags are scoped to your API key. Other keys cannot see or query your tags, even on the same attestation.

Census — Leak Detection & Inventory Stats

Compare suspect hashes against your organization's attested inventory for leak detection, and retrieve aggregate inventory statistics. Both endpoints require an API key with org_id configured.

javascript
const { CertiSigmaClient } = require('@certisigma/sdk');

const client = new CertiSigmaClient({ apiKey: process.env.CERTISIGMA_API_KEY });

// Leak detection — compare suspect hashes against org inventory
// Requires `scan` scope + org_id
const suspectHashes = ['a665a459...', 'b4c9a289...', 'e3b0c442...'];
const result = await client.scan(suspectHashes);
console.log(`Scan ${result.scan_id}: ${result.matched}/${result.total_scanned} matched`);
result.matches.forEach(m =>
  console.log(`  ${m.hash_hex} first_seen=${m.first_seen} source=${m.source}`)
);

// Inventory statistics — aggregate view of your org's claims
// Requires `batch` scope + org_id
const stats = await client.getBulkStats();
console.log(`Org ${stats.org_id_hash}: ${stats.total_claims} claims, ${stats.unique_hashes} unique`);
Object.entries(stats.claims_by_month).forEach(([month, count]) =>
  console.log(`  ${month}: ${count} claims`)
);

Scan uses a temp-table set intersection for performance on large inputs. The maximum number of hashes per scan is configured server-side (default 50K). Results include source and first_seen timestamp per match. Rate limited to 5 scans/hour per key.
Inventory stats returns total claims, unique hashes, date range, and monthly breakdown — scoped to your organization.

Census — Derived Lists

Create opaque HMAC-SHA256 derived lists for third-party hash verification without revealing your organization's hash inventory. Lists are signed by CertiSigma's ECDSA key for integrity verification. Requires census scope and org_id on the API key.

javascript
(async function run() {
  // 1. Create a derived list from explicit hashes
  const dl = await client.createDerivedList({
    hashes: ['a665a459...', 'b4c9a289...'],
    label: 'Partner audit Q1',
    expiresInHours: 720,
  });
  console.log(`List ID: ${dl.id}`);
  console.log(`List key (save now!): ${dl.list_key}`);

  // 2. Or create from tag filter
  const dl2 = await client.createDerivedList({
    tagFilter: { and: [{ key: 'department', value: 'hr' }] },
    label: 'HR confidential subset',
  });

  // 3. Third party: match files against the list
  // (can be done WITHOUT an API key — only needs list_key)
  const { CertiSigmaClient } = require('@certisigma/sdk');
  const { createHmac } = require('node:crypto');
  const pub = new CertiSigmaClient();

  const fileHash = 'a665a459...';
  const derived = createHmac('sha256', Buffer.from(dl.list_key, 'hex'))
    .update(fileHash).digest('hex');

  const result = await pub.matchDerivedList(dl.id, dl.list_key, [derived]);
  console.log(`Matched: ${result.matched}/${result.total}`);

  // 4. Verify list signature (public)
  const sig = await pub.getDerivedListSignature(dl.id);
  console.log(`Signed by: ${sig.signing_key_id}`);

  // 5. Owner: list, inspect, revoke
  const lists = await client.listDerivedLists();
  const detail = await client.getDerivedList(dl.id);
  const log = await client.getDerivedListAccessLog(dl.id);
  await client.revokeDerivedList(dl.id);
})();
ConstraintValue
Max items per list100,000 (server-configured)
Max TTL2,160 hours (90 days, server-configured)
Max hashes per match50,000 (server-configured)
Match rate limit20 requests/hour per list (server-configured)
Match endpoint authPublic (requires list_key)
Signature endpoint authPublic
Required scopecensus
Required key configorg_id must be set
Cryptographic properties: Derived hashes are HMAC-SHA256 transformations — the third party cannot reverse-engineer the original hashes. Lists are ECDSA-signed by CertiSigma, verifiable via GET /keys. Lists can be time-limited and revoked at any time.

Read Metadata

Explicit metadata read without re-verifying the attestation:

javascript
(async function run() {
  const meta = await client.getMetadata('att_42');
  console.log(`Source: ${meta.source}, Extra: ${JSON.stringify(meta.extra_data)}`);
})();

Requires metadata:read scope. Share tokens with the matching attestation ID can also call this endpoint.

Verification Examples

javascript
(async function run() {
  // 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}`));
})();

Public Verification (No API Key)

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.

javascript
(async function run() {
  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}`);
})();
Public vs authenticated verify: Without API key, verify returns only cryptographic proof (hash, timestamp, level, signature) — no source or extra_data. With API key, it also returns your claim's metadata when you own a claim on that hash.

verify, batchVerify, status, getEvidence, and health work without a key. Calling attest, batchAttest, updateMetadata, or deleteMetadata without an apiKey throws AuthenticationError immediately — the request is never sent.

status() Response Shape

Returns the current trust tier of an attestation without the full evidence payload. Useful for dashboards, progress tracking, and lightweight polling:

FieldTypeDescription
idstringAttestation ID (att_...)
hash_hexstringSHA-256 hash
level"T0" | "T1" | "T2"Current trust tier
t0_timestampstringECDSA signature timestamp
t1_timestampstring?TSA / Merkle batch timestamp
t2_timestampstring?Bitcoin anchor timestamp
t2_bitcoin_blocknumber?Bitcoin block height
signature_availablebooleanT0 ECDSA signature present
merkle_proof_availablebooleanT1 Merkle proof present
ots_availablebooleanOTS proof downloadable

verify vs status vs getEvidence

MethodInputAuthReturnsBest for
verify(hash)SHA-256 hashOptionalExistence, level, timestamp, signatureCompliance checks, "does this hash exist?"
status(attId)Attestation IDNoTrust tier, availability flags, timestampsDashboards, progress tracking, polling
getEvidence(attId)Attestation IDNoFull T0+T1+T2 proofs, Merkle path, OTSIndependent verification, long-term archival
Need claim isolation rules? See Security & Privacy Model for the full public/private boundary. See the API Reference for exact HTTP semantics.

Rate Limits

The API enforces per-key rate limits using a sliding window:

LimitDefaultNotes
Requests / minute1,000Configurable per key by admin
Monthly quotaUnlimitedOptional, per plan
Batch max size100Hashes per batch call

When rate-limited, the API returns 429 Too Many Requests with these headers:

  • Retry-After: 60
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

The SDK automatically throws RateLimitError with a retryAfter property.

javascript
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');
}

Public Endpoint Protection

Public endpoints (verify, status, getEvidence) do not require an API key and are not subject to per-key rate limits. They are protected at the infrastructure level:

LayerScopeLimit
ApplicationPer API key1,000 req/min (authenticated endpoints only)
InfrastructurePer client IPReverse proxy rate limiting on all endpoints (public and authenticated)

Abusive traffic (automated scraping, enumeration floods) is mitigated by infrastructure-level IP rate limiting. Legitimate verification traffic at normal volumes is never throttled.

Polling vs Webhooks

Attestations progress through levels asynchronously. Two patterns to track upgrades:

Polling (simple, low-volume)

javascript
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');
}

Webhooks (production, real-time)

Register a webhook via the API (POST /webhook/register) to get notified when an attestation reaches T1 or T2.

EventWhenPayload includes
t1_completeTSA timestamp obtainedattestation_id, hash_hex, tsa_timestamp
t2_completeBitcoin anchor confirmedattestation_id, hash_hex, bitcoin_block, confirmed_at

Delivery Headers

Each webhook delivery includes these HTTP headers:

HeaderDescription
X-CertiSigma-EventEvent type (t1_complete or t2_complete)
X-CertiSigma-DeliveryUnique numeric delivery ID — use for deduplication
X-CertiSigma-Signaturesha256=<hex> HMAC-SHA256 of the raw JSON body

Verify the X-CertiSigma-Signature header using the SDK helper (constant-time HMAC-SHA256):

javascript
const { verifyWebhookSignature } = require('@certisigma/sdk');

const isValid = verifyWebhookSignature(rawBody, signatureHeader, signingSecret);
// Uses crypto.timingSafeEqual internally — timing-attack resistant
Retry policy: Failed deliveries are retried 3 times with exponential backoff (1 min, 5 min, 15 min). Each delivery has a 10-second timeout. Use listWebhookDeliveries(webhookId) or GET /webhook/{id}/deliveries to inspect delivery history.

Delivery Semantics

  • At-least-once delivery: Your endpoint may receive the same event more than once (e.g. on network retries). Use attestation_id from the payload as an idempotency key to deduplicate.
  • Ordering: Deliveries are sent in best-effort chronological order but ordering is not guaranteed. Do not rely on delivery order for state transitions.
  • Anti-replay: Check the created_at timestamp in the payload and reject deliveries older than your tolerance window (e.g. 5 minutes). The delivery ID (X-CertiSigma-Delivery) can also be tracked to detect replays.

Consumer Reference Pattern

javascript
const seenDeliveries = new Set();  // In production, use Redis or a DB table
const MAX_AGE_MS = 300000;        // 5-minute anti-replay window

function handleWebhook(headers, body, rawBody) {
  const deliveryId = headers['x-certisigma-delivery'];
  if (seenDeliveries.has(deliveryId)) return 200;  // Idempotent ACK

  const age = Date.now() - new Date(body.created_at).getTime();
  if (age > MAX_AGE_MS) return 200;  // Stale — ACK to stop retries

  if (!verifyWebhookSignature(rawBody, headers['x-certisigma-signature'], secret))
    return 401;

  processEvent(body.attestation_id, headers['x-certisigma-event']);
  seenDeliveries.add(deliveryId);
  return 200;
}
Event vs delivery identity: attestation_id + event type identifies the event (what happened). X-CertiSigma-Delivery identifies the delivery attempt (retries share the same delivery ID). Deduplicate on delivery ID to handle retries; use attestation ID for business logic.

Secret Rotation

To rotate a webhook signing secret: register a new webhook with the same URL and events, verify it receives deliveries, then delete the old webhook. In-flight retries for the old webhook continue using the original secret.

Best practice: Use webhooks as the primary notification channel and polling as a fallback for missed events.

Webhook Management

The SDK exposes 4 methods for managing webhook subscriptions and a helper for verifying delivery signatures. All methods require an API key with the webhook scope.

Register a Webhook

javascript
const result = await client.registerWebhook(
  'https://my-server.com/census-hook',
  ['t1_complete', 't2_complete'],
  { label: 'census-watch-prod' },  // optional, for identification
);
// result.id             → "wh_abc123"
// result.signing_secret → "a1b2c3..." (shown once — store securely)
// result.label          → "census-watch-prod"

List Webhooks

javascript
const result = await client.listWebhooks();
result.webhooks.forEach(wh => console.log(wh.id, wh.url, wh.label, wh.failure_count));

Delete a Webhook

javascript
const result = await client.deleteWebhook('wh_abc123');
// result.deleted → true

List Delivery History

javascript
const result = await client.listWebhookDeliveries('wh_abc123');
result.deliveries.forEach(d => console.log(d.event_type, d.status, d.response_code));

Verify Webhook Signature

javascript
const { verifyWebhookSignature } = require('@certisigma/sdk');

// In your webhook handler (Express, Fastify, etc.):
const isValid = verifyWebhookSignature(
  req.rawBody,                              // Buffer
  req.headers['x-certisigma-signature'],    // "sha256={hex}"
  signingSecret,                            // from registerWebhook().signing_secret
);
ConstraintValue
Eventst1_complete, t2_complete
Signing secret64-char hex, shown once at registration — store securely
URL schemeHTTPS required in production; HTTP allowed in demo mode
LabelOptional, max 200 characters
Delivery historyLast 100 deliveries per webhook

Evidence & OTS Verification

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.

Recommended Workflow

  1. Register a t2_complete webhook (or poll with status())
  2. When notified, call getEvidence(attId)
  3. Save the updated manifest including T2 data
  4. Optionally save the .ots proof file for independent verification
javascript
(async function run() {
  const { 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));
}
})();

Evidence Response Fields

FieldTypeDescription
hash_hexstringSHA-256 hash of the attested data
levelstringHighest confirmed level: T0, T1, or T2
t0 — ECDSA Signature (immediate)
t0.signaturestringBase64-encoded ECDSA P-256 signature
t0.algorithmstringECDSA-P256-SHA256
t0.publicKeyIdstringSigning key identifier for key pinning
t0.registeredAtstringISO 8601 timestamp of registration
t1 — TSA Timestamp + Merkle Proof (minutes)
t1.batchIdnumberT1 batch identifier
t1.merkleRootstringMerkle root hash of the batch
t1.merkleProofArrayMerkle inclusion proof path (leaf → root)
t1.tsaProviderstringTSA provider (e.g. sectigo_qualified)
t1.tsaTimestampstringRFC 3161 timestamp (ISO 8601)
t1.tsaTokenstringBase64 TSA token for independent verification
t2 — Bitcoin Anchor via OpenTimestamps (hours)
t2.dailyRootstringDaily Merkle root anchored to Bitcoin
t2.t1ToT2ProofArrayMerkle proof linking T1 batch root to T2 daily root
t2.bitcoinBlockHeightnumberBitcoin block number containing the anchor
t2.bitcoinBlockHashstringBitcoin block hash
t2.bitcoinTxidstringBitcoin transaction ID
t2.confirmedAtstringISO 8601 confirmation timestamp
t2.otsProofstringRaw OpenTimestamps proof (Base64-encoded binary)

SDK Helper Functions

FunctionInputReturns
getBlockchainUrl(evidence)EvidenceResultmempool.space block URL, or null
getBlockchainUrl(evidence, 'tx')EvidenceResultmempool.space transaction URL, or null
saveOtsProof(evidence, path)EvidenceResult, stringtrue if saved, false if T2 not ready

Independent OTS Verification

The saved .ots file is a standard OpenTimestamps proof. Verify it independently — no CertiSigma involvement needed:

bash
# 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
Full chain of trust: Your hash → T0 ECDSA signature → T1 Merkle tree + TSA timestamp → T2 Bitcoin blockchain anchor. Each tier is independently verifiable. The .ots proof ties directly to a Bitcoin block — immutable, decentralized, permanent.
Blockchain explorer: Use getBlockchainUrl(evidence) to get a direct link to mempool.space, where anyone can independently verify the Bitcoin block and transaction containing your anchor.
Need verification details? See Test Vectors for canonical T0/T1/T2 verification examples with real production data. See Security & Privacy Model for the full public/private data boundary.

Error Handling

javascript
const { AuthenticationError, RateLimitError, QuotaExceededError } = require('@certisigma/sdk');

// Inside an async function
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');
  }
}

Error Envelope

All API errors return a consistent JSON envelope:

json
{ "error": true, "code": "ERROR_CODE", "message": "Human-readable description", "details": null }

Error Codes & Retry Guidance

HTTPCodeRetryableAction
400 / 422VALIDATION_ERRORNoFix request payload
401AUTH_ERRORNoCheck API key
403FORBIDDENNoInsufficient permissions or scope
404NOT_FOUNDNoResource does not exist
429RATE_LIMITYesBack off using Retry-After header
500INTERNAL_ERRORYesRetry with exponential backoff
Retry strategy: Only retry on 429 and 5xx. Fail fast on all other 4xx errors — they indicate a client-side issue that retrying will not resolve. The SDK exposes err.statusCode and err.retryAfter (on RateLimitError) for implementing backoff logic.

Example Error Responses

json — 429 Rate Limit
{ "error": true, "code": "RATE_LIMIT", "message": "Rate limit exceeded", "details": null }
// Headers: Retry-After: 12, X-RateLimit-Remaining: 0
json — 401 Auth Error
{ "error": true, "code": "AUTH_ERROR", "message": "Invalid or missing API key", "details": null }
json — 422 Validation Error
{ "error": true, "code": "VALIDATION_ERROR", "message": "hash_hex must be exactly 64 lowercase hex characters", "details": null }
json — 500 Internal Error
{ "error": true, "code": "INTERNAL_ERROR", "message": "An internal error occurred", "details": null }
Request tracing: Every API response includes an X-Request-Id header. You can send your own X-Request-Id in the request — the API echoes it back. If omitted, the server generates one automatically. Use it for technical support/debugging — include it when contacting support. For domain audit, use attestation_id — it identifies the attested record, not the HTTP request.

Security Best Practices

  • Environment variables: Store API keys in CERTISIGMA_API_KEY, never in source code.
  • Client-side encryption: Use encryptMetadata() for sensitive data in extraData.
  • Key rotation: Use rotateKey(envelope, oldKey, newKey) to re-encrypt without decrypting on the server.
  • Data isolation: Attestation IDs are sequential and public by design. Public endpoints (verify, getEvidence, status) return only cryptographic proof (hash, signature, Merkle, OTS). Organizational metadata (source, extraData) is never exposed publicly — only the authenticated API key owner can read their own claim data.
  • IP Allowlist: Restrict API key usage to known IP ranges.
  • Webhook secrets: Store the signing_secret in a secrets manager. It is shown only once.
  • Audit trail: Log all attestation IDs and hashes locally for independent auditing.

When to Use What

Use caseRecommended approach
One-off file attestationattestFile(path)
Serial number / identifier attestationattestString(text) — hash client-side, no temp file
Bulk ingestion pipelinehashFile() + batchAttest()
Audit / compliance checkverify() or batchVerify() (no API key needed)
Verify a specific file or stringverifyFile(file) or verifyString(text)
Leak detection (exfiltration)scan(suspectHashes) — compare against org inventory
Inventory dashboardgetBulkStats() — total claims, monthly breakdown
Long-term proof exportgetEvidence() + saveOtsProof()
Sensitive metadataClient-side encryption with encryptMetadata()
High-volume async notificationsWebhooks (t1_complete, t2_complete), not polling
Blockchain verification URLgetBlockchainUrl(evidence)

T0 Signature Format

Every attestation receives an immediate ECDSA-P256-SHA256 signature (T0). To verify independently:

Payload componentEvidence JSON fieldInvariant
format_versiont0.formatVersionSemver string, currently 2.0.0
hash_hexhash_hexLowercase hex, exactly 64 characters (SHA-256)
registered_att0.registeredAtISO 8601, microsecond precision, UTC offset +00:00
(e.g. 2026-03-16T12:00:00.000000+00:00)
Naming convention: The payload uses snake_case labels (format_version, registered_at). The evidence JSON response uses camelCase (t0.formatVersion, t0.registeredAt). When reconstructing the payload for verification, read the camelCase fields from the evidence and place them into the pipe-separated payload.

Payload construction: {format_version}|{hash_hex}|{registered_at} — pipe U+007C, no whitespace, UTF-8 encoded.

Signing: SHA-256 of the UTF-8 payload bytes, then ECDSA P-256 over the hash (prehashed). Signature is base64-encoded DER.

Independent Verification Steps

  1. Retrieve public keys from GET /keys.
  2. Match the key using t0.publicKeyId from your evidence.
  3. Reconstruct payload: `${evidence.t0.formatVersion}|${evidence.hash_hex}|${evidence.t0.registeredAt}`
  4. Compute SHA-256 of the UTF-8 payload bytes.
  5. Verify the ECDSA-P256-SHA256 signature (t0.signature, base64-decoded DER) using the JWK public key.

Server-Side Key Rotation

GET /keys returns all active signing keys. Old attestations reference the key that was active at signing time. Always match via t0.publicKeyId; never assume a single key.

Test Vectors

Canonical test vectors from a production attestation (att_500). Use these to validate any independent verification implementation against real CertiSigma output.

T0 — ECDSA Signature Verification

FieldValue
hash_hex7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac
t0.formatVersion2.0.0
t0.registeredAt2026-02-28T00:33:19.291619+00:00
t0.publicKeyIdcertisigma-3cb3ab64f5cfc983
t0.signatureMEQCIGuKNpN1MERfn7jeJW4aRG2om7nIp2eRz/GHueM/Hi9qAiA2ik/6not2lJUWnYGpQ6x9xouOt8j8Cr42vBVSrSeN+Q==

Step 1 — Reconstruct canonical payload

text — canonical payload (103 bytes UTF-8)
2.0.0|7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac|2026-02-28T00:33:19.291619+00:00

Step 2 — SHA-256 of canonical payload

text — expected SHA-256
f5203ed4089a3bd3367f9301de43db93b574e92e1d597227c738d5338cd5ae01

Step 3 — Public key (JWK from GET /keys)

json — P-256 public key
{
  "kty": "EC", "crv": "P-256", "alg": "ES256",
  "kid": "certisigma-3cb3ab64f5cfc983",
  "x": "7AKMIyF5Rm3SfCASYsJsBHvZhF9cqtjEcmbu5DL8wA8",
  "y": "94l-xPf8HVkp03XELgIeDvsBDtWTL8tj7glvfjFU1Fk"
}

Step 4 — Verify ECDSA-P256-SHA256

javascript — full verification (Node.js / WebCrypto)
const crypto = require('crypto');

// Evidence fields
const hashHex    = '7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac';
const version    = '2.0.0';
const registered = '2026-02-28T00:33:19.291619+00:00';
const sigB64     = 'MEQCIGuKNpN1MERfn7jeJW4aRG2om7nIp2eRz/GHueM/Hi9qAiA2ik/6not2lJUWnYGpQ6x9xouOt8j8Cr42vBVSrSeN+Q==';

// Public key JWK
const jwk = {
  kty: 'EC', crv: 'P-256', alg: 'ES256',
  x: '7AKMIyF5Rm3SfCASYsJsBHvZhF9cqtjEcmbu5DL8wA8',
  y: '94l-xPf8HVkp03XELgIeDvsBDtWTL8tj7glvfjFU1Fk',
};

(async () => {
  // Import public key
  const key = await crypto.subtle.importKey(
    'jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']
  );

  // Reconstruct canonical payload (WebCrypto hashes internally — do NOT pre-hash)
  const payload = new TextEncoder().encode(`${version}|${hashHex}|${registered}`);

  // DER → raw r||s (WebCrypto expects IEEE P1363, not DER)
  const derBuf = Buffer.from(sigB64, 'base64');
  const rLen = derBuf[3];
  const r = derBuf.subarray(4, 4 + rLen);
  const sStart = 4 + rLen + 2;
  const s = derBuf.subarray(sStart);
  const raw = Buffer.alloc(64);
  r.copy(raw, 32 - r.length);
  s.copy(raw, 64 - s.length);

  // Pass payload (not digest!) — WebCrypto computes SHA-256 internally
  const ok = await crypto.subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' }, key, raw, payload
  );
  console.log(ok ? '✅ T0 signature verified' : '❌ verification failed');
})();
Prehashed vs standard ECDSA: CertiSigma signs the SHA-256 digest using a prehash mode. WebCrypto subtle.verify always hashes internally — pass the raw UTF-8 payload, not the digest. If you pass a pre-computed SHA-256 digest, WebCrypto double-hashes it and verification fails. The SHA-256 shown in Step 2 is provided for auditing the canonical payload, not as input to signature verification.

T1 — Merkle Inclusion Proof (RFC 6962 v2)

FieldValue
Leaf hash7fd9101e...cf0aac (same as hash_hex)
Merkle root1ccb4cfd5ea666d2f7418f104cebf200ef258b784dc483698335a084cf0f1d67
Merkle version2 (RFC 6962 domain separation: H(0x01 || left || right))
Proof path3 steps (see below)
text — proof traversal
Leaf: 7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac

Step 0: H(0x01 || 63792cf5...b8e026 || 7fd9101e...cf0aac) → 844370b7ddafe4f9...
Step 1: H(0x01 || 82950bd8...631955 || 844370b7...e4f9..) → 4529ddc8d19457a9...
Step 2: H(0x01 || 4529ddc8...57a9.. || a35f3d52...27f35e) → 1ccb4cfd5ea666d2...

Computed root: 1ccb4cfd5ea666d2f7418f104cebf200ef258b784dc483698335a084cf0f1d67 ✅
javascript — Merkle proof verification
const crypto = require('crypto');

const leaf = '7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac';
const root = '1ccb4cfd5ea666d2f7418f104cebf200ef258b784dc483698335a084cf0f1d67';
const proof = [
  { hash: '63792cf512a74247fe1ac05e25494f40143fe0f6da9b1db855e9444035b8e026', position: 'left' },
  { hash: '82950bd8a3b929aaf12905e2457c91240c9f479a64888a1bd53fbdee23631955', position: 'left' },
  { hash: 'a35f3d526cbffff89d8099580271cb855109e84d357ed4cf31a46f7d0427f35e', position: 'right' },
];

let current = Buffer.from(leaf, 'hex');
for (const step of proof) {
  const sibling = Buffer.from(step.hash, 'hex');
  const prefix = Buffer.from([0x01]);
  current = step.position === 'left'
    ? crypto.createHash('sha256').update(Buffer.concat([prefix, sibling, current])).digest()
    : crypto.createHash('sha256').update(Buffer.concat([prefix, current, sibling])).digest();
}

console.log(current.toString('hex') === root
  ? '✅ Merkle inclusion verified'
  : '❌ Merkle proof failed');

T2 — Bitcoin Anchor

FieldValue
Daily rootb7e7f6bfcd2419864d5021df4a0c9345faa48b9a4de0e1f45ee6158ffb5e02e0
Bitcoin block938792
Confirmed at2026-03-01T03:53:18.079007+00:00
OTS proofBinary proof available via GET /attestation/att_500/ots/proof
T2 independent verification: Download the .ots proof file and verify with any OpenTimestamps-compatible tool (e.g. ots verify att_500.ots). The proof chains from the daily Merkle root to a Bitcoin block header, providing mathematical proof of existence before that block's timestamp.

Negative Test Vector (must fail)

Same attestation with registeredAt altered by one microsecond (291619291620). Any correct implementation must reject this signature:

javascript — expected failure
const altered = new TextEncoder().encode(
  '2.0.0|7fd9101e1dce01c87570d767b4b87a6e608250253c9d8c77fdd1b15731cf0aac|2026-02-28T00:33:19.291620+00:00'
);
const bad = await crypto.subtle.verify(
  { name: 'ECDSA', hash: 'SHA-256' }, key, raw, altered
);
console.log(bad ? '❌ BUG: should have failed' : '✅ Correctly rejected altered timestamp');

Machine-Readable Test Vectors

All test vectors are available as a single JSON file for automated testing:

GET /test-vectors.json — contains T0 (positive + negative), T1, and T2 data with all raw values copy-safe.

Need exact HTTP semantics? See the API Reference for full OpenAPI specification, request/response schemas, and error codes.

Security & Privacy Model

CertiSigma is a public attestation platform. Cryptographic proofs are intentionally public; organizational metadata is strictly private. This section formalizes the boundary.

CategoryVisibilityDetails
Attestation IDsPublicSequential (att_1, att_2, …). Discoverable by design — any party can look up any ID.
SHA-256 hashPublicThe attested hash is visible on public endpoints. It does not reveal file contents (preimage resistance).
ECDSA signature (T0)PublicVerifiable by anyone via GET /keys.
Merkle proof (T1)PublicInclusion proof is publicly verifiable.
OTS / Bitcoin anchor (T2)PublicDownloadable .ots proof, verifiable against Bitcoin blockchain.
TimestampsPublicregisteredAt, t1_timestamp, t2_timestamp are always visible.
sourcePrivateOnly returned to the authenticated API key owner who created the claim.
extraDataPrivateOnly returned to the authenticated API key owner. Use encryptMetadata() for defense in depth.
TagsPrivateScoped per API key. Other keys cannot see or query your tags, even on the same attestation. Supports client-side encryption for sensitive values.
Claim existencePrivatePublic endpoints show the attestation exists, not which organizations hold claims on it.

Threat Model

The sequential nature of attestation IDs means an external party can enumerate all attestation IDs and retrieve their public cryptographic proofs. This is accepted by design because:

  • CertiSigma is an attestation platform — public verifiability is the core value proposition.
  • Public endpoints return only cryptographic proof (hash, signature, Merkle, OTS). No organizational metadata is ever exposed.
  • The SHA-256 hash is a one-way function — knowing the hash does not reveal the attested content.
  • For sensitive metadata, client-side encryption (AES-256-GCM) ensures the server itself never sees plaintext.

What the Server Sees

DataServer has access?
SHA-256 hashYes — required for attestation
Original file / contentNever — only the hash is sent
source (optional label)Yes, if provided — plaintext string, not encryptable. Do not include PII.
extraData (plaintext)Only if the client chooses not to encrypt. The SDK supports AES-256-GCM client-side encryption — use it for any sensitive metadata.
extraData (client-encrypted)Never — server stores only the AES-256-GCM ciphertext, cannot decrypt
API key identityYes — required for authenticated endpoints
Client IP (general API)Seen at transport level — used for infrastructure rate limiting. Not stored in any application-level database table for normal API operations (attest, verify, metadata, tags).
Client IP (forensic sharing)When a third party accesses data via share token or performs a derived list match, the IP is recorded in the access log. This log is visible only to the data owner (the API key holder who created the token/list). This enables forensic audit of who accessed shared data.
Recommendation: If your extraData contains PII or business-sensitive information, always use client-side encryption. This provides defense in depth: even if the server were compromised, the attacker would only see ciphertext. source is always plaintext but optional and never publicly visible — only you (the authenticated API key owner) can read it. If in doubt, omit it.

Configuration Reference

ParameterDefaultDescription
apiKeyundefinedBearer token (cs_live_... or cs_demo_...). Optional for verify/health.
baseUrlhttps://api.certisigma.chAPI endpoint
timeout30000Request timeout in ms

Requirements

  • Node.js 18+ or any modern browser with fetch support
  • Zero external dependencies

Advanced Configuration

TopicDetails
ProxyPlatform-dependent. In Node.js, set HTTP_PROXY / HTTPS_PROXY or pass a custom fetch with proxy support (e.g. undici).
Custom fetchPass a custom fetch implementation to the constructor for proxies, logging, or test mocking.
Custom TLS CASystem defaults. For custom CAs, set NODE_EXTRA_CA_CERTS=/path/to/ca.pem environment variable.
RetryNot built-in. Implement at caller level with exponential backoff on 429 / 5xx (see Error Handling above).
ObservabilityNo built-in hooks. Wrap fetch for request/response logging. Redact the API key from logged headers.

Resilient Client Wrapper

Reference implementation with retry, backoff, and logging:

javascript
const { CertiSigmaClient, RateLimitError, CertiSigmaError } = require('@certisigma/sdk');

async function resilientCall(fn, { retries = 3 } = {}) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const result = await fn();
      console.log(`OK attempt=${attempt + 1}`);
      return result;
    } catch (err) {
      if (err instanceof RateLimitError) {
        const wait = (err.retryAfter || 2 ** attempt) * 1000;
        console.warn(`429 retryAfter=${err.retryAfter} attempt=${attempt + 1}`);
        await new Promise(r => setTimeout(r, wait));
      } else if (err instanceof CertiSigmaError && err.statusCode >= 500 && attempt < retries - 1) {
        const wait = 2 ** attempt * 1000;
        console.warn(`5xx status=${err.statusCode} attempt=${attempt + 1}`);
        await new Promise(r => setTimeout(r, wait));
      } else {
        throw err;
      }
    }
  }
  throw new Error('Max retries exceeded');
}

const client = new CertiSigmaClient({ apiKey: process.env.CERTISIGMA_API_KEY });
const result = await resilientCall(() => client.attest(hash, { source: 'pipeline' }));

Sandbox vs Production

AspectSandbox (cs_demo_...)Production (cs_live_...)
ECDSA signingEphemeral key (may rotate on restart)Persistent key pair
T1 (TSA)Free TSA providerQualified TSA
T2 (Bitcoin)Same anchoring pipelineSame anchoring pipeline
Webhook URLshttp:// allowedhttps:// required
Rate limit1,000/min (default)1,000/min (configurable per key/org)
Data retentionPersistent (same DB)Persistent
OpenAPI explorerAvailable at /docsDisabled

To promote an integration from sandbox to production, replace the apiKey with a cs_live_... key. No code changes required.

Package

npm   @certisigma/sdk  —  MIT License — Ten Sigma Sagl

Compatibility Policy

  • Semver: This SDK follows Semantic Versioning 2.0.0. Patch releases contain only bug fixes; minor releases add features without breaking changes; major releases may contain breaking changes.
  • API stability: The SDK targets API /v1/. When a new API version is introduced (e.g. /v2/), the previous version will remain supported for a minimum of 12 months.
  • SDK–API mapping: SDK v1.x targets API v1. New API features are supported in minor SDK releases.
  • Support window: The latest minor release receives bug fixes and patches. The previous minor release receives critical security patches for 6 months after the next minor release.
  • Deprecation: Deprecated features are marked in the changelog and emit warnings for at least one minor version before removal.

Changelog

v1.10.0

  • New: hashString() — SHA-256 of UTF-8 strings (no temp file needed)
  • New: attestString() — hash + attest in one call (source never auto-populated)
  • New: verifyString() — hash + verify for strings
  • New: verifyFile() — hash + verify for File/Blob/ArrayBuffer/Uint8Array
  • Cross-SDK determinism: Python and JavaScript produce identical hashes for the same string input

v1.6.0

  • New: createShareToken(), listShareTokens(), getShareTokenInfo(), revokeShareToken() — forensic metadata sharing with time-limited, auditable tokens
  • New: putTags(), getTags(), deleteTag(), queryTags() — structured multi-dimensional tagging with server-side AND queries (max 10 conditions)
  • New: getMetadata() — explicit metadata read without re-verifying
  • New: RBAC scoped keys — metadata:read, metadata:write, tags:read, tags:write, share
  • New: batchVerify({ detailed: true }) — returns full claim metadata in batch mode (requires API key)

v1.5.0

  • New: request ID header (X-Request-ID) included in all requests

v1.4.0

  • New: getBlockchainUrl(evidence) — returns a mempool.space URL for the Bitcoin block or transaction
  • New: saveOtsProof(evidence, path) — saves the raw OpenTimestamps .ots proof to a local file

v1.3.0

  • New: apiKey is now optional — public endpoints (verify, batchVerify, health) work without authentication
  • New: AuthenticationError thrown immediately if auth-required method called without key (no request sent)

v1.2.0

  • New: standalone hashFile() and hashBytes() utility functions
  • New: @certisigma/sdk/hash subpath export

v1.1.1 — Initial Public Release

  • Full API coverage: attest, verify, batch, file attestation, metadata, evidence, status, health
  • Client-side AES-256-GCM encryption via Web Crypto API with key rotation
  • Typed error hierarchy: AuthenticationError, RateLimitError, QuotaExceededError
  • Full TypeScript definitions (index.d.ts, crypto.d.ts)
  • Works in Node.js 18+ and modern browsers — zero dependencies