Skip to main content
0byte
Browse docs

Build with 0byte

Content Credentials for AI content, as one API call. Stamp embeds a signed C2PA manifest and anchors a derived fingerprint in a public transparency log; verify reads the evidence back — free, for anyone, even from a screenshot.

Base URLhttps://api.0byte.techREST + Python & TypeScript SDKsOpenAPI spec ↗Changelog

Overview

0byte is a managed C2PA signing service: origin proofs for AI content that survive screenshots, re-encoding, and stripped metadata. Two verbs cover the flow:

  • Stamp — at generation, embed a signed C2PA manifest (Content Credentials) in the file and anchor a derived perceptual fingerprint in a public, append-only transparency log. Your brand goes in the manifest.
  • Verify — check any content, free and without a key. It reads standard Content Credentials from any signer — not just 0byte's — and matches fingerprints against the registry. The verdict is built from that evidence, never from a model's guess.

Quickstart

Install an SDK (or use plain HTTP — the API is the contract):

pip install zerobyte

Stamp a generation, publish the stamped file, and verify anything later:

from zerobyte import Client client = Client(api_key="0b_key_...") # 1 · Stamp — Content Credentials + a signed registry proof, with your brand on it result = client.stamp( content=open("art.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, always disclosed with open("art.stamped.png", "wb") as f: f.write(result.stamped_bytes) # publish THIS file — it carries the manifest # 2 · Verify — free, evidence-first, no API key check = Client().verify(open("downloaded.png", "rb").read()) print(check.verdict) # "verified_origin" print(check.origin_sources) # ["manifest", "registry"]

Publish the stamped file, not the original

The response's stamped_content is your file with the Content Credentials embedded. Publish that one — the original bytes carry no manifest (the registry fingerprint still covers them).

Need an API key?

Stamping requires a key. Grab one from your dashboard — join the waitlist for access. Verify, proof lookups, and transparency are public.

Authentication

Get a key from your terminal — pip install zerobyte && zerobyte login opens a browser approval and hands the key to your CLI. It is the only way keys are minted; the dashboard and /v1/keys list and revoke. Stamp and key-management requests then authenticate with a bearer token:

text
Authorization: Bearer 0b_key_...

Keys carry scopes, and each authenticated endpoint requires one: stamp for stamping, analyze for the deprecated estimate endpoint, and keys for key management. A key without the required scope gets 403 SCOPE_DENIED.

Public by design

Verify, proof lookup, and the transparency endpoints need no key — anyone can independently check a record, which is the whole point of a public registry. CORS is open, so you can call them straight from a browser.

Rate limits

Per API key, per minute:

TierLimit
Free60 req/min
Dev300 req/min
Pro1,000 req/min

Every response to a key on a rate-limited tier carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds at which one unit of capacity returns — the window slides over the last minute). Over the limit returns 429 RATE_LIMITED with Retry-After in seconds.

Keyless endpoints are limited per IP

Verify, proof lookup, and transparency accept bursts of up to 60 requests per IP, refilling at 1 request per second. Requests carrying an API key use a separate per-IP lane — a 1,000-request burst refilling at about 17 per second — so the tier limit, not the address, is what binds a key. Over either returns 429 IP_RATE_LIMITED with Retry-After. High-volume verification belongs behind an API key.

How it works

Every stamp creates two bindings at once. The C2PA manifest travels inside the file — standard Content Credentials any C2PA validator can read. The perceptual fingerprint is derived from the pixels — never embedded — and anchored in the public transparency log, so origin survives even when metadata is stripped.

Stamp

Embed a signed manifest with your brand as the claim generator, and anchor the derived fingerprint in the signed, append-only log.

Verify

Read the manifest if present, match the fingerprint against the registry, and return a verdict built only from that evidence.

Why two bindings?

Manifests are strong but strippable — most platforms remove metadata on upload. Fingerprints survive stripping but need a registry. Together, one covers the other's failure mode.

Content Credentials

The embedded manifest is standard C2PA. It names your brand as the claim generator (“generated by Acme Studios using imagen-x”), declares AI origin via the IPTC trainedAlgorithmicMedia source type, and carries an org.0byte.proof assertion that links back to the registry record.

Manifest embedding by format:

FormatsBinding
JPEG · PNG · WebP · GIF · TIFFmanifest+registry
BMP · ICOregistry_only — no C2PA embed support; the fingerprint still covers them
Audio · VideoNot accepted at launch — 415 with the supported list; images only, no overclaiming

Two assertions, one field

creator is written into the manifest twice: as the C2PA claim_generator, which is what a validator shows as the producing tool, and as a stds.schema-org.CreativeWork assertion whose author is an Organization carrying your name and, if you send one, your url. The first names you in C2PA's own vocabulary; the second says it in terms the wider web already reads.

Asserted, not verified — and we say so

The creator field is an asserted identity: a claim by whoever holds the API key. Verify responses label it as such. Cryptographically verified identity (CAWG) is a separate, later tier.

Trust-list status, honestly

Until 0byte's signing certificate is accepted onto the C2PA trust list, other validators show 0byte-signed manifests as an untrusted signer. Verify discloses the same — and the registry match independently carries the verified_origin verdict for stamped content in the meantime.

Fingerprinting

0byte matches content with perceptual hashing rather than exact byte matching. Two visually-identical images produce near-identical fingerprints, so a match holds even after JPEG re-encoding, resizing, or a screenshot — exactly the cases where embedded metadata is destroyed.

The fingerprint (version v2) is a 64-bit perceptual hash: luma → 32×32 → 2-D DCT → the top-left 8×8 low-frequency block → each coefficient compared to the median. Every proof records its fingerprint_version; only the current version takes part in matching.

Blank images have no fingerprint to bind

An image with essentially no luminance variation — a white frame, a flat colour, near-flat noise — hashes to numerical noise shared by every such image. Stamping one returns 422 CONTENT_TOO_UNIFORM, and verifying one reports the registry signal as skipped rather than risking a match by accident. Anything with real content — a logo on a white background included — fingerprints normally.
The registry compares fingerprints by Hamming distance (0 = exact); the returned match_confidence reflects how close the match is. It is a match-quality number — not an AI-likelihood estimate.
Stamping registers more than one fingerprint per asset: the original plus the crops the internet predictably makes — square, 4:5 and 16:9 feed crops, a trimmed border, a chopped caption or UI bar. A match reports which one in matched_as (original, aspect4x5, bottom10, …), so a hit through a crop is disclosed as one. Deeper trims, off-centre windows and cut-out details do not match yet.

Verdicts

Verify returns one of four verdicts, each traceable to evidence in the same response. There is no “92% AI” anywhere in a verdict — a probability is a guess, and guesses don't survive appeals.

VerdictMeaning
verified_originA registry match, or a valid manifest from a trusted signer. origin_sources says which.
provenance_untrustedA valid manifest whose signer does not chain to the trust list — a self-claim, disclosed as such, never promoted.
provenance_invalidA manifest is present but fails validation — tampered or corrupted.
no_provenance_foundNo manifest and no registry match. Absence of evidence — not a fake detector.

Honest failure beats a silent one

If a signal can't be evaluated, its status says error in the evidence. If verification itself can't run, the API returns 503 VERIFICATION_UNAVAILABLE instead of a degraded verdict dressed up as an answer.

Want an AI-likelihood estimate anyway? That's the opt-in analyze endpoint — always separate, never part of a verify verdict.

Transparency log

Every proof is appended to a single append-only Merkle tree (RFC 6962-style) and the signed root is republished as the tree grows — new proofs are anchored within about a minute. Anyone can fetch an inclusion proof to confirm a record was committed and never altered, and a consistency proof to confirm history was never rewritten. No blockchain — Ed25519 signatures over an append-only tree.

See the transparency endpoints — or verify the log yourself in ~30 lines of Python.

Python SDK

A thin client over the REST API — httpx + pydantic, no ML on your side.

bash
pip install zerobyte

Stamp

python
result = client.stamp( content=image_bytes, content_type="image/png", provider="acme-ai", model="imagen-x", creator={"name": "Acme Studios"}, # optional brand attribution ) result.binding # "manifest+registry" | "registry_only" — always disclosed result.stamped_bytes # the manifest-embedded file to publish (None if registry_only) result.verify_url # public proof page

Verify

python
check = client.verify(open("photo.png", "rb").read()) check.verdict # "verified_origin" | "provenance_untrusted" # | "provenance_invalid" | "no_provenance_found" check.origin_sources # which signals prove it, e.g. ["manifest", "registry"] check.evidence # per-signal detail — statuses always disclosed

Transparency

python
head = client.get_tree_head() # signed root (Ed25519) inc = client.get_inclusion_proof(pid) # Merkle audit path keys = client.get_signing_keys() # every key that ever signed

Errors are typed and carry the API's code: catch RateLimitError (429) and the retryable VerificationUnavailableError (503); ZerobyteError is the base.

TypeScript SDK

Zero-dependency client for Node and edge runtimes, fully typed.

bash
npm install zerobyte
typescript
import { Zerobyte } from "zerobyte"; const zb = new Zerobyte({ apiKey: "0b_key_..." }); const result = await zb.stamp({ content, contentType: "image/png", provider: "acme-ai", model: "imagen-x" }); // → StampResult const check = await new Zerobyte().verify(bytes); // → VerificationResult, no key await zb.getTreeHead(); // signed root await zb.getInclusionProof(proofId); // Merkle audit path await zb.getConsistencyProof(64, 128); await zb.getSigningKeys();

Same typed errors as Python: RateLimitError, retryable VerificationUnavailableError, and the ZerobyteError base carrying code and status.

API reference

All endpoints live under https://api.0byte.tech and return JSON. The machine-readable contract is served at GET /v1/openapi.yaml. Within /v1 changes are additive only — existing fields never change meaning or disappear — and every addition is recorded in the changelog.

Stamp

POST/v1/stampAPI key

Embed Content Credentials and create a signed registry proof in one call. The response always discloses how the proof is bound.

FieldTypeDescription
contentreqstringBase64-encoded image (max 10 MB).
content_typereqstringMIME type. image/png, image/jpeg, image/webp, image/gif, image/tiff embed a manifest; image/bmp and image/x-icon fall back to registry-only. Anything else — or a declared type that doesn't match the bytes — is refused with 415 and the supported list.
providerreqstringAI provider, e.g. acme-ai.
modelreqstringModel identifier, e.g. imagen-x.
creatorobjectBrand attribution: {"name", "url?"} — asserted twice in the manifest, as the C2PA claim generator and as the schema.org author. url travels with the author, so the credential carries a link back to you.
metadataobjectPublic. Stored on the proof record, which anyone can fetch — never put prompts, PII, or secrets here.
curl -X POST https://api.0byte.tech/v1/stamp \ -H "Authorization: Bearer 0b_key_..." \ -H "Content-Type: application/json" \ -d '{ "content": "<base64_encoded_media>", "content_type": "image/png", "provider": "acme-ai", "model": "imagen-x", "creator": {"name": "Acme Studios", "url": "https://acme.example"}, "metadata": {"prompt_hash": "abc123"} }'

Retries are safe: send an Idempotency-Key

The log is append-only, so a duplicated retry would be a permanent duplicate proof. Send a unique Idempotency-Key header per asset (a UUID is fine): a retry with the same key and the same request returns the original proof with replayed: true; the same key with a different request is refused with 409. Keys are scoped to your API key and expire after 24 hours. Without the header every call mints a new proof — a retried timeout included.

Verify

POST/v1/verifyPublic

The evidence-first meta-verifier: reads the embedded manifest, matches the fingerprint against the registry, and returns a verdict built only from that evidence. Free, no key.

FieldTypeDescription
contentreqstringBase64-encoded image (max 10 MB).
content_typereqstringMIME type of the content.
curl -X POST https://api.0byte.tech/v1/verify \ -H "Content-Type: application/json" \ -d '{ "content": "<base64_encoded_image>", "content_type": "image/png" }'
  • verdict — see Verdicts.
  • origin_sources — which signals support the verdict: manifest, registry.
  • evidence — per-signal detail; a signal that could not be evaluated says so instead of disappearing.
  • matched, confidence — legacy registry-match fields kept for compatibility. confidence is fingerprint match quality, never an AI-likelihood estimate.
  • 503 VERIFICATION_UNAVAILABLE — verification could not run; retry. Never a degraded verdict.

Analyze

POST/v1/analyzeDeprecatedAPI key

Opt-in estimate — REST-only, not in any SDK

Analyze returns a classifier estimate of AI likelihood. It is a guess by design, kept out of /v1/verify verdicts entirely — and deliberately absent from both SDKs, so a probability is never one autocomplete away from the evidence-first API. Call it directly over HTTP if you explicitly want the estimate.
bash
curl -X POST https://api.0byte.tech/v1/analyze \ -H "Authorization: Bearer 0b_key_..." \ -H "Content-Type: application/json" \ -d '{ "content": "<base64_encoded_image>", "content_type": "image/png" }'

Get proof

GET/v1/proofs/:idPublic

Fetch a proof record by its ID. Everything an independent verifier needs is in the response — including which signing key produced the signature and which canonical format it covers.

curl https://api.0byte.tech/v1/proofs/0b_a1b2c3d4-...

API keys

GET/v1/keysAPI key

List and revoke the keys in your account. Minting happens in the terminal — zerobyte login runs a device-code flow you approve in the browser; there is no creation endpoint, so a raw key never transits anything but your own CLI.

  • GET /v1/keys — list your account's active keys (raw keys are never returned).
  • DELETE /v1/keys/:id — revoke a key immediately (not the one making the request). Proofs it already stamped keep verifying.

Transparency

GET/v1/transparency/*Public

Independently confirm that a proof was committed to the signed log and that history was never rewritten.

Signed tree head — GET /v1/transparency/head

curl https://api.0byte.tech/v1/transparency/head

Inclusion proof — GET /v1/transparency/inclusion/:id

curl https://api.0byte.tech/v1/transparency/inclusion/0b_a1b2c3d4-...

Consistency proof — GET /v1/transparency/consistency/:from/:to

curl https://api.0byte.tech/v1/transparency/consistency/64/128

Check it with the algorithm in RFC 9162 §2.1.4.2 — proves the log at size 64 is a strict prefix of the log at size 128, i.e. history was never rewritten.

Signing keys — GET /v1/transparency/keys

curl https://api.0byte.tech/v1/transparency/keys
New proofs appear in the log within about a minute of stamping. Until then, the inclusion endpoint returns 404 NOT_YET_ANCHORED.

Drop it into your pipeline

Stamping is one authenticated POST after generation, wherever your stack runs it. Each recipe below is complete: an Idempotency-Key per asset makes retries safe against the append-only log, and the response's binding field says exactly what you got — publish stamped_content when it is present.

# pip install fastapi httpx uvicorn # The stamp() call is complete — wire it to your existing generation code. import base64, os, uuid import httpx from fastapi import FastAPI app = FastAPI() ZB = httpx.Client( base_url="https://api.0byte.tech", headers={"Authorization": f"Bearer {os.environ['ZEROBYTE_API_KEY']}"}, timeout=30, ) def stamp(image_bytes: bytes, *, model: str, asset_id: str) -> dict: """Call right after your model returns an image. Returns the bytes to publish (credentialed when signing is enabled) and the public proof URL. The Idempotency-Key makes retries safe: same asset, same proof.""" r = ZB.post( "/v1/stamp", headers={"Idempotency-Key": asset_id}, json={ "content": base64.b64encode(image_bytes).decode(), "content_type": "image/png", "provider": "acme-ai", "model": model, }, ) r.raise_for_status() proof = r.json() publish = ( base64.b64decode(proof["stamped_content"]) if proof.get("stamped_content") else image_bytes # binding: registry_only — the proof still stands ) return {"bytes": publish, "verify_url": proof["verify_url"], "id": proof["id"]} @app.post("/generate") def generate(prompt: str): image_bytes = my_model.generate(prompt) # ← your existing call # Mint one id per generated asset and store it alongside the asset: # retrying with the same id replays the same proof; deriving it from # the prompt would collide when two generations share a prompt. asset_id = uuid.uuid4().hex result = stamp(image_bytes, model="imagen-x", asset_id=asset_id) return {"proof": result["verify_url"]}

Verify the log yourself

Fetching is not verifying. This is a complete independent verifier — it checks the Ed25519 signature on the head, recomputes the leaf hash from the public proof record, and folds the Merkle audit path up to the signed root. If any line lies, an assert fails.

python
# pip install requests cryptography import base64, hashlib, requests from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey API = "https://api.0byte.tech" proof_id = "0b_..." # 1 · Signed head: check the key derives its key_id, then check the signature head = requests.get(f"{API}/v1/transparency/head").json() pub = bytes.fromhex(head["public_key"]) assert hashlib.sha256(pub).hexdigest()[:8] == head["key_id"] vk = Ed25519PublicKey.from_public_bytes(pub) sth0 = f'sth:v1:{head["key_id"]}:{head["tree_size"]}:{head["root_hash"]}' vk.verify(base64.b64decode(head["signature"]), sth0.encode()) # 2 · Inclusion proof: it carries its own signed (tree_size, root_hash) pair inc = requests.get(f"{API}/v1/transparency/inclusion/{proof_id}").json() assert inc["key_id"] == head["key_id"] # else resolve via /v1/transparency/keys sth = f'sth:v1:{inc["key_id"]}:{inc["tree_size"]}:{inc["root_hash"]}' vk.verify(base64.b64decode(inc["signature"]), sth.encode()) # 3 · Recompute the leaf hash from the PUBLIC proof record — no trust needed proof = requests.get(f"{API}/v1/proofs/{proof_id}").json() leaf = hashlib.sha256(b"\x00" + proof_id.encode() + proof["fingerprint"].encode() + proof["timestamp"].encode()).digest() assert leaf.hex() == inc["leaf_hash"] # 4 · Fold the audit path (RFC 9162 §2.1.3.2) up to the signed root f, s, r = inc["leaf_index"], inc["tree_size"] - 1, leaf for p in [bytes.fromhex(h) for h in inc["inclusion_path"]]: if f & 1 or f == s: r = hashlib.sha256(b"\x01" + p + r).digest() while f & 1 == 0 and f != 0: f >>= 1; s >>= 1 else: r = hashlib.sha256(b"\x01" + r + p).digest() f >>= 1; s >>= 1 assert s == 0 and r.hex() == inc["root_hash"] print("inclusion verified against a signed head — the math, not our word")
  • Leaf hash: SHA256(0x00 ‖ proof_id ‖ fingerprint ‖ timestamp); interior nodes are domain-separated with 0x01.
  • Head signature covers sth:v1:{key_id}:{tree_size}:{root_hash}, and key_id is the first 4 bytes of SHA256(public_key) — derivable, not trusted.
  • The path-folding algorithm is RFC 9162 §2.1.3.2 — the same one Certificate Transparency uses.
  • Key trust: deriving key_id proves the served key matches its identifier — not that it belongs to the log operator. Pin a known key, or compare /v1/transparency/keys across time to detect substitution.

Compliance mapping

The EU AI Act's transparency obligations (Article 50) require providers of generative AI systems to mark outputs as artificially generated in a machine-readable way, and for that marking to be detectable. Here is how each requirement maps to what a stamp gives you:

RequirementWhat 0byte provides
Machine-readable markingA standard C2PA manifest embedded at generation, declaring AI origin via the IPTC trainedAlgorithmicMedia source type — readable by any C2PA validator.
Marking that survives redistributionThe derived fingerprint in the public registry — origin remains recoverable after screenshots, re-encoding, and metadata stripping.
DetectabilityThe free /v1/verify endpoint and verify UI — anyone can check, no key, no account.
AuditabilityAn RFC 6962-style signed transparency log with public inclusion and consistency proofs — verifiable without trusting 0byte.

Capability mapping, not legal advice

This maps product capabilities to regulatory language. Whether your deployment satisfies a given obligation depends on your system and jurisdiction — confirm with counsel.

Error codes

Errors return {"code", "message", "docs_url", "request_id"} with the matching HTTP status; 415s also carry supported, the list of media types that would have been accepted. Every response — errors included — carries an X-Request-Id header: yours, echoed, when it matches [A-Za-z0-9._-]{1,128}; otherwise a generated one. Quote it when you write in.

CodeStatusMeaning
AUTH_MISSING401No Authorization header
AUTH_INVALID401API key is invalid or revoked
SCOPE_DENIED403Key lacks the required scope
KEY_NOT_ACCOUNT_BOUND403Key management needs an account-bound key
RATE_LIMITED429Too many requests for your tier
INVALID_INPUT400A request field failed validation
INVALID_BASE64400Content is not valid base64
INVALID_JSON400Body is not the JSON this endpoint expects (malformed, missing or mistyped field)
NOT_FOUND404No such route — the API lives under /v1
INVALID_IMAGE400Content is not a decodable image
UNSUPPORTED_MEDIA_TYPE415content_type is outside the supported list (carried in the response)
CONTENT_TYPE_MISMATCH415content_type is supported but the bytes are another format
IDEMPOTENCY_KEY_INVALID400Idempotency-Key must be 1–255 printable characters
IDEMPOTENCY_KEY_REUSED409Idempotency-Key already used with a different request
IDEMPOTENCY_IN_PROGRESS409The original request with this key is still running — retry shortly
PAYLOAD_TOO_LARGE413Content exceeds the 10 MB limit
CONTENT_TOO_UNIFORM422Image has no perceptual content to fingerprint (blank or near-blank)
PROOF_NOT_FOUND404No proof with that ID
NOT_YET_ANCHORED404Proof not yet in the log (wait ~60s)
KEY_NOT_FOUND404No API key with that ID
CANNOT_DELETE_SELF400Can’t revoke the key making the request
VERIFICATION_UNAVAILABLE503Verification could not run — retry; never a degraded verdict
IP_RATE_LIMITED429Too many requests from this address (60-burst per IP, 1/s refill) — honour Retry-After
LAST_KEY400Cannot revoke the last key on the account — create another first
AUTHORIZATION_PENDING202CLI pairing: not approved yet, keep polling
SLOW_DOWN429CLI pairing: polling faster than the given interval
PAIRING_NOT_FOUND404CLI pairing: unknown code
PAIRING_EXPIRED410CLI pairing: expired — run login again
PAIRING_UNUSABLE409CLI pairing: code mistyped, expired, or already approved
PAIRING_ALREADY_USED409CLI pairing: key already collected — run login again
PAIRING_RATE_LIMITED429CLI pairing: too many logins from this address
INTERNAL_ERROR500Something failed on our side — quote the request_id

Ready to ship Content Credentials?

Get an API key and stamp your first generation in an afternoon.

Get access