Install
Two packages, both thin clients over the same API. Neither is required — the third tab is the whole integration in any language.
pip install zerobyteOne name everywhere: the package you install, the module you import and the command you sign in with are all zerobyte.
Stamping needs an API key — minted from your terminal with zerobyte login, then listed in your dashboard. Verifying never does.
Quickstart
Stamp at generation, publish the stamped file, verify it later — the whole loop, in three steps.
from zerobyte import Client
client = Client(api_key="0b_key_...")
# 1 · Stamp at generation — signs a C2PA manifest AND registers the fingerprint
result = client.stamp(
content=open("render.png", "rb").read(),
content_type="image/png",
provider="acme-ai",
model="imagen-x",
creator={"name": "Acme Studios", "url": "https://acme.example"},
)
print(result.binding) # "manifest+registry" — how the proof is bound
print(result.verify_url) # public proof page
# 2 · Publish the STAMPED file — that's the one carrying the credentials
if result.stamped_bytes:
with open("render.stamped.png", "wb") as f:
f.write(result.stamped_bytes)
# 3 · Verify anything, later — free, no API key
check = Client().verify(open("render.stamped.png", "rb").read())
print(check.verdict) # "verified_origin"
print(check.origin_sources) # ["manifest", "registry"]Publish the stamped file
The one mistake that silently loses your credentials
stamp() does not modify your input — it returns a new file with the manifest embedded. Publish stamped_bytes / stampedBytes, not the bytes you passed in. Ship the original and the Content Credentials are simply absent; the registry fingerprint still works, but every C2PA validator sees nothing.The response always discloses how the proof is bound, so you never have to guess which case you got:
| binding | What to publish |
|---|---|
manifest+registry | The returned stamped bytes — they carry the C2PA manifest, and the fingerprint is registered too. |
registry_only | Your original bytes. The format can't carry an embedded manifest, so the registry fingerprint covers it alone — binding_reason says why. |
result = client.stamp(content=data, content_type="image/bmp",
provider="acme-ai", model="imagen-x")
result.binding # "registry_only"
result.binding_reason # why: this format can't carry an embedded manifest
result.stamped_bytes # None — publish your original bytes; the
# registry fingerprint still covers themVerify: the evidence model
Verification reads Content Credentials from any signer and matches the fingerprint against the public registry. The verdict is built from that evidence — one of four values, never a probability.
| verdict | Meaning |
|---|---|
| verified_origin | A registry match, or a valid manifest from a trusted signer. |
| provenance_untrusted | Valid manifest, signer not on the C2PA trust list — a self-claim, disclosed as such. |
| provenance_invalid | A manifest is present but fails validation — altered after signing. |
| no_provenance_found | No manifest, no registry match. Absence of evidence — never an AI guess. |
check = Client().verify(image_bytes)
check.verdict # verified_origin | provenance_untrusted
# | provenance_invalid | no_provenance_found
check.origin_sources # which signals carry the verdict, e.g. ["registry"]
m = check.evidence["manifest"]
m["status"] # valid | invalid | absent | error
m["trusted"] # True only if the signer chains to the C2PA trust list
m["claim_generator"] # the brand named in the manifest
r = check.evidence["registry"]
r["status"] # match | no_match | skipped | error
r["distance"] # fingerprint Hamming distance (0 = exact)
check.is_verified_origin # convenience booleanabsent, not nothing. The legacy matched / confidence fields still exist and describe the registry signal only; confidence is fingerprint match quality, never an AI-likelihood estimate.If you specifically want a probability rather than evidence, a deprecated opt-in estimate endpoint still exists over plain HTTP — see the API reference. It is never part of a verdict, and deliberately absent from both SDKs.
API surface
The complete client surface in both languages, side by side.
| Python | TypeScript | Returns | Key |
|---|---|---|---|
stamp(content, content_type, provider, model, metadata?, creator?) | stamp({ content, contentType, provider, model, metadata?, creator? }) | StampResult — proof + binding + stamped bytes | required |
verify(content, content_type?) | verify(content, contentType?) | VerificationResult — verdict + evidence | not needed |
get_proof(proof_id) | getProof(proofId) | Proof | not needed |
get_tree_head() | getTreeHead() | Signed tree head | not needed |
get_inclusion_proof(proof_id) | getInclusionProof(proofId) | Merkle audit path | not needed |
get_consistency_proof(first, second) | getConsistencyProof(first, second) | Consistency path | not needed |
get_signing_keys() | getSigningKeys() | Signing-key registry | not needed |
list_keys() · revoke_key(key_id) — mint keys with zerobyte login | listKeys() · revokeKey(keyId) — mint keys with zerobyte login | API key management | required |
Python is synchronous and supports with Client(...) as client:; the TypeScript client is zero-dependency and runs in Node and edge runtimes.
Errors
Errors are typed and carry the API's code and status, so you can branch on them instead of parsing strings.
from zerobyte import (
Client, SDKError, AuthenticationError, ProofError,
VerificationError, RateLimitError, VerificationUnavailableError,
)
try:
result = client.stamp(content=data, content_type="image/png",
provider="acme-ai", model="imagen-x")
except RateLimitError:
... # 429 — back off and retry
except VerificationUnavailableError:
... # 503 — verification couldn't run; retry, never assume
except AuthenticationError:
... # 401/403 — bad key or missing scope
except SDKError as e:
... # base class; e.code and e.status carry the API's answer503 is a retry, not a verdict
VerificationUnavailableError means verification could not run. It is deliberately an error rather than a degraded answer — retry it, and never treat it as “nothing found”.Transparency
Every proof is anchored in a signed, append-only log. The SDKs fetch everything an independent verifier needs:
head = client.get_tree_head() # signed root (Ed25519)
inc = client.get_inclusion_proof(result.id) # Merkle audit path
keys = client.get_signing_keys() # every key that ever signedFetching is not verifying
The SDKs deliberately ship no crypto. To check the math yourself, recompute the Merkle path and verify the Ed25519 signature over the signed head — there's a complete worked verifier in the docs.Next: the full API reference, how this maps to the EU AI Act, or try verification in the browser — free, no account.

