BitSeal
API reference

Two endpoints. Plain JSON.

The public API is small on purpose. Seal a manifest, look up a root, walk the transparency log. POST /api/seal requires a proof-of-work token (see GET /api/challenge below) and a Cloudflare Turnstile token for browsers (the SDK and other programmatic callers identify themselves via X-API-Client and bypass the Turnstile step only). The other endpoints are read-only and per-IP rate-limited. All responses are JSON, all hashes are lowercase hex.

Base URL

https://bitseal.orygn.tech
POST/api/seal

Accepts a precomputed manifest (root hash, leaves, metadata, proof-of-work token) and returns the signed seal plus a PDF certificate. The server re-derives the root from the supplied leaves before signing, so a client cannot pin a signature to a root that does not match its own leaves. Every call requires a valid pow_token obtained from GET /api/challenge below; browser callers also need a Cloudflare Turnstile token.

Request body
root_hash*string64-char lowercase hex, the Merkle root of the file.
merkle_tree*string[]Leaf hashes in file order, lowercase hex.
seal_mode*string"merkle-blake3-64k-v2" (default, recommended for new integrations) or "merkle-blake3-64k-v1" (legacy). The signature layout dispatches off this field.
chunk_size_bytes*numberMust be 65536. Identical for v1 and v2.
blake3_hash*stringWhole-file BLAKE3 digest.
sha3_512_hash*stringWhole-file SHA3-512 digest.
entropy*numberShannon entropy of the file, 0.0 to 8.0.
filename*stringAdvisory, stored as metadata. For v2 seals it is bound by the signature; for v1 seals it is unsigned.
size_bytes*numberFile size in bytes.
mime_type*stringAdvisory MIME type.
pow_token*objectProof-of-Work token: { challenge_id, nonce }. Obtain from GET /api/challenge, solve SHA-256(challenge || str(nonce)) for the required leading zero bits, then submit here. Bound to the requesting IP and single-use.
tokenstringCloudflare Turnstile token. Required for browser callers; optional when the X-API-Client header is set.
200 response
successbooleanAlways true for 200.
seal_idstringUnique ID for the seal record.
seal_indexnumber | nullThe seal's monotonically-increasing position in the BitSeal Merkle log (0-based). Stable identifier you can pass to /api/log/inclusion later to retrieve a Merkle inclusion proof against any signed tree head that includes this seal. Null only in pathological insertion-race cases.
root_hashstringEcho of the request body's root_hash, after server-side re-derivation from the supplied leaves.
signaturestring128-char hex Ed25519 signature. For v2 seals, covers the SHA3-512 of the canonical manifest with the signature field removed. For v1 seals, covers the 40-byte root || LE_f64(timestamp).
timestampnumberUTC unix seconds with fractional precision, the server-generated time the seal was issued and the value the signature binds to.
seal_modestringEcho of the request body's seal_mode. Verifiers dispatch by this field.
leaf_countnumberNumber of leaves in the recomputed Merkle tree.
signerstringHuman-readable signer label (currently "Orygn Authority").
pdf_base64stringBase64-encoded PDF certificate.
otsobject | nullOpenTimestamps submission summary at seal time: digest, calendars[], attempted_at. Null only when all four calendars failed at submission. The Bitcoin-anchored upgrade is exposed via /api/verify after the daily cron sweep.
Example request
curl -X POST https://bitseal.orygn.tech/api/seal \
  -H "Content-Type: application/json" \
  -H "X-API-Client: acme-sealer/1.2" \
  -d '{
    "root_hash": "6d56f7...",
    "merkle_tree": ["a1b2c3...", "d4e5f6..."],
    "seal_mode": "merkle-blake3-64k-v2",
    "chunk_size_bytes": 65536,
    "blake3_hash": "...",
    "sha3_512_hash": "...",
    "entropy": 7.91,
    "filename": "contract.pdf",
    "size_bytes": 130000,
    "mime_type": "application/pdf",
    "pow_token": { "challenge_id": "...", "nonce": "..." }
  }'

For the easiest integration use the official Python SDK (github.com/OrygnsCode/BitSeal-SDK), which transparently handles the challenge/solve/submit round-trip and the X-API-Client header.

GET/api/challenge

Returns a fresh proof-of-work challenge bound to the requesting client IP. Solve it locally by computing SHA-256(challenge || str(nonce)) until the digest has at least difficulty leading zero bits, then submit { challenge_id, nonce } as the pow_token field on POST /api/seal. At the default difficulty of 18 a solution takes approximately 250 ms on a modern laptop. Challenges expire after 5 minutes and each is valid for exactly one seal.

200 response
challenge_idstring (32 hex)Opaque identifier. Pass this back unchanged in pow_token.challenge_id.
challengestring (64 hex)The 32 random bytes (hex-encoded) that the SHA-256 input must begin with.
difficultynumberRequired leading zero bits in the SHA-256 digest of the concatenation. Default 18; operator may raise under attack via the POW_DIFFICULTY env var, so a client must read this value rather than hard-coding it.
expires_atstring (ISO 8601)Absolute UTC instant after which this challenge will be rejected.
ttl_secondsnumberSeconds until expiry (mirrors expires_at).
hash_inputstringHuman-readable description of the hash input: "SHA-256(challenge || str(nonce))." The challenge is treated as the 64-character hex STRING, not the raw 32 bytes.
Status codes
200OKChallenge issued.
400Bad requestClient IP could not be resolved.
429Rate limitedPer-IP rate limit exceeded. Check Retry-After header.
503UnavailableProof-of-Work backend is not configured (operator-side outage).
Example request
curl "https://bitseal.orygn.tech/api/challenge"
GET/api/verify

Looks up a root on the public ledger. Returns the three-axis verdict: ledger presence, Ed25519 signature validity, and Merkle tree consistency. Each axis is reported on its own field.

If you only check one field, check all_axes_pass. The legacy valid field is always trueon a 200 response: it means "the root was found in the ledger and a verdict was produced," not "the seal passed every cryptographic check." A naive consumer that writes if (res.valid) accept() will treat a tampered or signature-failed seal as good. Use all_axes_pass as the single boolean trust gate, or read the per-axis fields directly to apply your own policy.

Query parameters
root*string64-char lowercase hex root to look up.
tokenstringCloudflare Turnstile token. Required for browser callers; optional when the X-API-Client header is set.
200 response
validbooleanAlways true on a 200 response. Means the root was found in the ledger and a verdict was produced, NOT that the seal passed every cryptographic check. Kept for backward compatibility; prefer all_axes_pass for new integrations. When the root is not found, the response is 404 with valid:false.
all_axes_passbooleanRecommended trust gate for third-party integrators. True iff signature_verified is true AND the Merkle tree axis is OK (tree_checked is false because no leaves were stored, OR tree_checked is true and tree_consistent is true). Mirrors the web UI's overall verdict. False indicates at least one cryptographic axis failed or could not be checked; read per-axis fields below to see which.
seal_indexnumber | nullThe seal's monotonically-increasing position in the BitSeal Merkle log (0-based). Pass this value to /api/log/inclusion to retrieve a Merkle inclusion proof for any signed tree head that included this seal. Null only in pathological cases (the column is backfilled for every historical seal).
signature_verifiedbooleanAxis 2. Did the Ed25519 signature verify against the resolved Authority key (current or historical). False both when verification failed (tamper, malformed manifest) and when the seal was signed by a CLI key the web verifier does not hold.
signature_check_attemptedbooleanTrue iff the verifier attempted the Ed25519 check (the seal claims to be web-signed). False iff the seal was signed by a CLI Authority key and the web verifier holds no matching key. Combined with signature_verified this distinguishes "check failed" (attempted=true, verified=false, e.g. tamper) from "check skipped" (attempted=false, verified=false).
verification_key_idstringWhich Authority key ID verified, or was attempted.
signature_notestring | nullShort human-readable note when axis 2 is inconclusive or failed.
tree_checkedbooleanAxis 3. Whether the Merkle fold was re-run (only possible when leaves are stored).
tree_consistentboolean | nullAxis 3 verdict. True if the re-folded root matches the stored root; null when tree_checked is false.
tree_notestring | nullShort note when axis 3 is inconclusive or skipped.
seal_modestringThe manifest format of the record. Verifiers dispatch by this field.
leaf_countnumberNumber of leaves in the stored Merkle tree.
otsobjectOpenTimestamps anchor state. Sub-fields: status ("upgraded" | "pending" | "none"), digest, calendars[], submitted_at, block_height, block_time, upgraded_proof_base64.
dataobjectThe full stored manifest record (every manifest field as documented in /docs/architecture).
Status codes
200OKA verdict was produced. Read fields to interpret.
400Bad requestMissing token, malformed root.
404Not foundThe root is not in the ledger.
429Rate limitedPer-IP or global rate limit exceeded. Check Retry-After header.
Example request
curl "https://bitseal.orygn.tech/api/verify?root=6d56f7...&token=<turnstile>"

Transparency log

Every seal occupies a monotonic seal_index position in a Merkle log. The Authority periodically signs a Signed Tree Head (STH) committing the BitSeal ledger to a particular Merkle root over every seal up to that moment. Each STH's signature is submitted to OpenTimestamps and Bitcoin-anchored within 1 to 24 hours, so deletion or omission of any committed seal becomes cryptographically detectable against a public Bitcoin block. The three endpoints below let any holder of a seal walk the math themselves and independently confirm their seal's inclusion. Full spec at spec/log-sth.md.

GET/api/log/heads/latest

Returns the most-recently-published Signed Tree Head. The shape mirrors a single element of /api/log/heads below. No Turnstile gating; per-IP rate limited.

200 response
sth.sth_indexnumberMonotonic position of this STH in the log, starting at 0 (genesis).
sth.anchor_at_utcstringISO8601 instant the STH was computed and signed.
sth.seal_countnumberNumber of seals committed by this STH's Merkle root.
sth.merkle_rootstring (64 hex)BLAKE3 Merkle root over leaf hashes of every seal up to seal_count.
sth.prev_sth_merkle_rootstring (64 hex)merkle_root of the previous STH. 64 zero hex chars for the genesis STH.
sth.signaturestring (128 hex)Ed25519 signature over SHA3-512 of the canonical STH bytes (see spec/log-sth.md §5).
sth.signerstring"Orygn Authority" for production STHs.
sth.seal_modestring"sth-v1". Versioning anchor for future spec revisions.
sth.otsobjectOpenTimestamps anchor state: digest, calendars, has_pending, has_upgraded, block_height, block_time. Raw OTS proof bytes are NOT returned by this endpoint.
Status codes
200OKLatest STH returned.
404Not foundNo STH has been published yet (the log is empty).
429Rate limitedPer-IP rate limit exceeded.
Example request
curl "https://bitseal.orygn.tech/api/log/heads/latest"
GET/api/log/heads

Returns recent Signed Tree Heads, ordered by sth_index descending. Pagination via before_sth_index.

Query parameters
limitnumberNumber of STHs to return. Integer in [1, 200]. Default 50.
before_sth_indexnumberReturns only STHs with sth_index strictly less than this value. Use for pagination.
200 response
countnumberNumber of STHs in this response.
sthsobject[]Each element has the same shape as /api/log/heads/latest's sth object.
paginationobject{ limit, has_more, next_before_sth_index }. When has_more is true, pass next_before_sth_index back as before_sth_index to fetch the next page.
Example request
curl "https://bitseal.orygn.tech/api/log/heads?limit=10"
GET/api/log/inclusion

Returns a Merkle inclusion proof showing that a given seal was committed by a Signed Tree Head. The response includes the seal's three log-leaf fields (so a verifier can recompute the leaf hash locally), the target STH, and the proof itself. Anyone can fold the proof against the STH's merkle_root using a generic BLAKE3 library; see web/scripts/inclusion-verify-third-party-demo.mjs in the repo for a 95-line reference implementation with no BitSeal-specific dependencies.

Query parameters
seal_index*numberNon-negative integer. The seal's monotonic position in the log. Returned by POST /api/seal and GET /api/verify.
sth_indexnumberDefaults to the latest STH if omitted. Currently only the latest STH is supported for inclusion proofs; historical STHs return 501.
200 response
sealobject{ seal_id, seal_index, signature }. The three fields a verifier needs to recompute the leaf hash per spec §3.
sthobjectThe target STH. Same shape as /api/log/heads/latest's sth object.
leaf_hashstring (64 hex)BLAKE3 leaf hash of the seal. A verifier should recompute this locally from the seal fields and confirm it matches before walking the proof.
proofobject[]Array of { sibling_hash, position } steps from leaf to root. position is 'left' or 'right'. Fold per the verify_recipe field.
verify_recipestringOne-line algorithm: walk the proof, BLAKE3(sibling || node) or BLAKE3(node || sibling) depending on position, end at sth.merkle_root.
spec_urlstringPermalink to spec/log-sth.md §7 for the full normative description of the proof format.
Status codes
200OKProof returned.
400Bad requestseal_index missing, malformed, or out of range for the target STH.
404Not foundNo STH has been published yet, or sth_index does not exist.
429Rate limitedPer-IP rate limit exceeded.
500InternalServer-side ledger consistency check failed (recomputed Merkle root does not match the STH). This is operator-side cheating evidence; please report.
501Not implementedInclusion proofs against historical STHs are not yet supported. Use the latest STH (omit sth_index) or use the most recent value.
Example request
curl "https://bitseal.orygn.tech/api/log/inclusion?seal_index=39"

Programmatic access (SDK, scripts)

Programmatic callers should set a non-empty X-API-Client header on every request. The server uses it as an integration identifier in logs, as an additional rate-limit dimension (see below), and as the signal that lets the caller skip the Cloudflare Turnstile widget gate, which is only useful for browsers that can render the challenge. The proof-of-work requirement still applies, header or not, the SDK handles the challenge round-trip transparently.

The reference Python SDK sends X-API-Client: BitSeal-SDK/<version> python/<ver> on every request. Third-party integrations should set their own identifier, e.g. X-API-Client: acme-sealer/1.2. Allowed format is [A-Za-z0-9_-./+ ], 1 to 128 characters. Malformed values are rejected with a 400.

Rate limits

Limits are enforced over sliding windows. The server returns 429 with a Retry-After header and a retry_after_seconds field in the JSON body. An IP that hits a honeypot path is silently blocklisted for 24 hours and the same 429 shape is returned, so a probing scanner learns nothing about why it was throttled.

Per-IP limits
POST /api/sealburst10 requests per minute.
POST /api/sealhourly3 requests per hour.
GET /api/challengeburst+hourlyShares the /api/seal burst+hourly budget (a challenge fetch only makes sense paired with a seal).
GET /api/verifyburst100 requests per minute.
GET /api/verifyhourly60 requests per hour.
GET /api/log/headsburst+hourlyShares the verify burst+hourly limits.
GET /api/log/heads/latestburst+hourlySame shared limits as /api/log/heads.
GET /api/log/inclusionburst+hourlySame shared limits as /api/log/heads.
Per X-API-Client limit
POST /api/sealhourly5 seals per hour per distinct X-API-Client value. Closes the IP-rotation dodge where an attacker keeps one identifier but changes IPs.
Global limits
POST /api/sealhourly50 seals per hour across all IPs. Distributed-abuse ceiling, well within operational budget.

Edge protection in front of these app-level limits: Cloudflare Bot Fight Mode + JS Detections + a zone-level rate-limit rule (5 requests per IP per Cloudflare colo per 10 seconds on /api/seal). Legitimate browser and SDK traffic is unaffected.

Need higher throughput for an integration, a research audit, or a production batch? Email [email protected] and describe the use case.

Offline verification

The cryptography here is standard and published. Given the manifest, the public key at /.well-known/bitseal-authority-key.json, and a BLAKE3 plus Ed25519 library, anyone can verify a seal with no network call to BitSeal. The Python SDK does this, and its source is the normative reference.