Skip to main content
0byte
SDKs

Content Credentials in a few lines of code.

One call signs a spec-compliant C2PA manifest and registers a fingerprint that survives stripping. Python and TypeScript wrap the same REST API — and the API is the contract, so any language works.

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 zerobyte

One 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:

bindingWhat to publish
manifest+registryThe returned stamped bytes — they carry the C2PA manifest, and the fingerprint is registered too.
registry_onlyYour 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 them

Verify: 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.

verdictMeaning
verified_originA registry match, or a valid manifest from a trusted signer.
provenance_untrustedValid manifest, signer not on the C2PA trust list — a self-claim, disclosed as such.
provenance_invalidA manifest is present but fails validation — altered after signing.
no_provenance_foundNo 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 boolean
Both signals are always reported, including when one is absent or errored — a missing manifest says absent, 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.

PythonTypeScriptReturnsKey
stamp(content, content_type, provider, model, metadata?, creator?)stamp({ content, contentType, provider, model, metadata?, creator? })StampResult — proof + binding + stamped bytesrequired
verify(content, content_type?)verify(content, contentType?)VerificationResult — verdict + evidencenot needed
get_proof(proof_id)getProof(proofId)Proofnot needed
get_tree_head()getTreeHead()Signed tree headnot needed
get_inclusion_proof(proof_id)getInclusionProof(proofId)Merkle audit pathnot needed
get_consistency_proof(first, second)getConsistencyProof(first, second)Consistency pathnot needed
get_signing_keys()getSigningKeys()Signing-key registrynot needed
list_keys() · revoke_key(key_id) — mint keys with zerobyte loginlistKeys() · revokeKey(keyId) — mint keys with zerobyte loginAPI key managementrequired

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 answer

503 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 signed

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