Skip to content

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 exception
recipes = 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, and created_at assertions
  • 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.c2pa
Verifier 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_uri
response = 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_intact
Confidence: 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 manifest
from PIL import Image
img = Image.open("my_image.jpg")
cropped = img.crop((0, 0, 400, 400))
cropped.save("cropped.jpg")
# Look up by perceptual fingerprint
lookup_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 status
verification = 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:

Terminal window
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:

  1. Signed an image with a C2PA manifest and TrustMark watermark
  2. Inspected the manifest assertions
  3. Verified the signed asset and read the proves/does_not_prove fields
  4. Viewed the result in the public verifier UI
  5. Looked up a modified copy by perceptual fingerprint
  6. 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.