Your First Sign
Your First Sign
This guide walks through the complete sign and verify lifecycle for a single image. By the end you will have a signed asset with a C2PA manifest, an embedded TrustMark watermark, and a verifiable URL you can share.
What you need
- A Verbitas API key (get one at verbitas.io/onboard)
- Python 3.10+ with
pip install verbitas - A JPEG or PNG image
Step 1: Set up the client
import verbitas
# The SDK reads VERBITAS_API_KEY from the environment.# Export it before running: export VERBITAS_API_KEY=vb_live_...client = verbitas.Client()Confirm the key works:
# This should not raise an exceptionrecipes = client.recipes.list()print([r.id for r in recipes])# ['image-genai-v1', 'image-editorial-v1', ...]Step 2: Choose a recipe
For an AI-generated image, use image-genai-v1. This recipe:
- Embeds a TrustMark watermark
- Builds a C2PA manifest with
ai_generated,generator,model, andcreated_atassertions - Anchors to OpenTimestamps (Bitcoin Tier 1)
- Retains the manifest for 365 days
recipe = client.recipes.get("image-genai-v1")print(recipe.recipe_yaml)Step 3: Sign the image
result = client.sign( "my_image.jpg", recipe="image-genai-v1", metadata={ "generator": "stable-diffusion-xl", "model": "sdxl-1.0", # Never include prompt text; only its hash if needed: # "prompt_hash": "sha256:e3b0c442..." })
print(f"Asset ID: {result.asset_id}")print(f"Watermark ID: {result.watermark_id}")print(f"Manifest URI: {result.manifest_uri}")print(f"Verifier URL: {result.verifier_url}")print(f"Anchor: {result.anchor}")Example output:
Asset ID: a_01j7abc...Watermark ID: w_01j7abd...Manifest URI: https://m.verbitas.io/manifests/a_01j7abc.../manifest.c2paVerifier URL: https://v.verbitas.io/v/a_01j7abc...Anchor: {'batch_id': 'b_01j7abe...', 'status': 'queued'}The watermark is now embedded in the image bytes. The original file on disk has been replaced with the watermarked version. Save the asset_id for later.
Step 4: Inspect the manifest
Download the raw C2PA manifest:
import requests, json
manifest_url = result.manifest_uriresponse = requests.get(manifest_url)# The manifest is a binary JUMBF file. The API also serves a JSON summary:manifest_json = requests.get(manifest_url + "?format=json").json()
print(json.dumps(manifest_json["assertions"], indent=2))Example assertions:
[ { "label": "c2pa.ai_generated", "data": { "value": true } }, { "label": "c2pa.generator", "data": { "value": "stable-diffusion-xl" } }, { "label": "c2pa.model", "data": { "value": "sdxl-1.0" } }, { "label": "c2pa.created_at", "data": { "value": "2026-05-09T10:00:00Z" } }]Step 5: Verify the signed image
verification = client.verify(asset_id=result.asset_id)
print(f"Status: {verification.status}")print(f"Confidence: {verification.confidence}")print()print("Proves:")for p in verification.proves: print(f" - {p}")print()print("Does not prove:")for p in verification.does_not_prove: print(f" - {p}")Expected output:
Status: verified_manifest_intactConfidence: 0.94
Proves: - The C2PA manifest was signed by a certificate on the Verbitas trust list. - The manifest has not been modified since signing. - The signing certificate was valid and not revoked at sign time.
Does not prove: - The assertions in the manifest were accurate when submitted. - The content is semantically truthful or unedited in all respects. - The file is legally admissible as evidence in any jurisdiction.Note: the watermark signal takes a few seconds to index after signing. If you verify immediately, you may get verified_manifest_intact rather than verified_manifest_and_watermark_match. Wait 10–30 seconds and re-verify to get the watermark signal.
Step 6: View in the public verifier
Open the verifier URL in a browser:
https://v.verbitas.io/v/a_01j7abc...The public verifier shows:
- The manifest status with a colour-coded badge (not “real”/“fake” — the explainable state label)
- All assertions from the manifest
- What the result proves and does not prove
- A link to download the full JSON report
This URL is publicly accessible without authentication. You can share it alongside any distributed copy of the image.
Step 7: Verify a modified copy
A key feature of Verbitas is soft-binding: finding the manifest for a copy that has been cropped or re-encoded.
# Simulate receiving a cropped version with no manifestfrom PIL import Image
img = Image.open("my_image.jpg")cropped = img.crop((0, 0, 400, 400))cropped.save("cropped.jpg")
# Look up by perceptual fingerprintlookup_result = client.lookup("cropped.jpg")for match in lookup_result.matches: print(f"Found: {match.asset_id} via {match.match_type} (confidence {match.confidence:.2f})")# Found: a_01j7abc... via perceptual_hash (confidence 0.83)The lookup confirms the crop came from the original signed asset, even though the manifest is gone.
Step 8: Check the anchor status
The anchor batch runs hourly. After ~60 minutes, check the anchor status:
# Re-verify to get updated anchor statusverification = client.verify(asset_id=result.asset_id)print(verification.signals.get("anchor_match"))# True (once the batch has been anchored)Or check the anchor batch directly:
curl -H "Authorization: Bearer $VERBITAS_API_KEY" \ https://api.verbitas.io/v1/anchors/b_01j7abe... | jq '{status, ots_confirmed}'Summary
After completing this guide you have:
- Signed an image with a C2PA manifest and TrustMark watermark
- Inspected the manifest assertions
- Verified the signed asset and read the
proves/does_not_provefields - Viewed the result in the public verifier UI
- Looked up a modified copy by perceptual fingerprint
- Confirmed the asset was included in an anchor batch
The Quickstart is a condensed version of steps 1–5 for copy-paste use. See the API Reference for the full parameter documentation.