originalpictures-sdk: Python SDK for C2PA Content Credentials, AI watermarking and verification
originalpictures-sdk is the official, typed Python client for the
Original Pictures API. Use it to sign images, audio, video and
documents with C2PA Content Credentials, add an invisible watermark and perceptual fingerprints,
and verify what a file declares about its origin.
It covers all 128 operations in the public API contract with generated types. Its synchronous and asynchronous clients execute the 126 consumer operations; the two provider-ingress operations (Clerk and Stripe callbacks) stay typed but are deliberately non-callable, because their signatures cover exact raw bytes that a JSON client must not re-encode.
from originalpictures import Client
with Client(base_url="https://api.originalpictures.com", api_key="opk_...") as client:
me = client.get_current_user()
capabilities = client.get_capabilities()
What it does
- C2PA signing. Every signing job writes a C2PA manifest (Content Credentials) into the file, or beside it for sidecar formats, following the C2PA Technical Specification 2.4. The manifest records the file's declared origin: who signed it, with which tool, and what actions were declared.
- Invisible watermark. Optionally embeds an invisible watermark before signing, so a copy whose metadata was stripped can still be matched to your record.
- Perceptual fingerprinting. Optionally keeps server-side fingerprints of the signed output for recovery by similarity.
- AI-generation declaration. Records that a generative model produced the file
(
c2pa.createdwith an IPTC digital source type such astrainedAlgorithmicMedia). This is the kind of machine-readable marking that EU AI Act Article 50(2) and the California AI Transparency Act (SB 942, as amended by AB 853) describe. - Verification. Checks Content Credentials, the certificate chain and your account's records, and reports the file's declared origin with reason codes. It is never a judgement about the content itself.
- Public verify pages.
verify_public()checks a file against an account's public verify page without an API key. - Webhooks.
verify_webhook()checks Standard Webhooks signatures on raw request bodies.
Why use it
- Fully typed (
py.typed): every request body, parameter and response is aTypedDict, so mypy and Pyright catch a misspelled field before the request is sent. - Safe transfers:
upload_fileanddownload_filestream bytes and finish only after the expected length and SHA-256 match. API authorization is never forwarded to upload or download URLs. - Bounded by default: response size limits, request timeouts, and polling helpers with a wall deadline and cooperative cancellation.
- Stable, typed errors (
AuthenticationError,RateLimitError,ConflictErrorand others) carrying the API's reasoncodeandrequest_id. - One dependency:
httpx.
Install
pip install originalpictures-sdk
Or with uv:
uv add originalpictures-sdk
Requires Python 3.12 or later.
Quickstart: sign a file
Get an API key from the console (keys start with
opk_). Upload the bytes, then accept a signing job for the asset.
import hashlib
import os
from pathlib import Path
from originalpictures import Client, idempotency_key, poll, sign_job_terminal
path = Path("photo.jpg")
digest = hashlib.sha256(path.read_bytes()).hexdigest()
with Client(
base_url="https://api.originalpictures.com",
api_key=os.environ["ORIGINALPICTURES_API_KEY"],
) as client:
upload = client.create_upload(
body={
"byte_length": path.stat().st_size,
"declared_media_type": "image/jpeg",
"mode": "single",
"sha256": digest,
}
)
target = upload.get("upload")
if target is None:
raise RuntimeError("a single-mode upload returns one presigned request")
client.upload_file(target["url"], path, sha256=digest, headers=target["headers"])
client.complete_upload(
body={},
parameters={"upload_id": upload["upload_id"]},
headers={"Idempotency-Key": idempotency_key()},
)
# C2PA only: no watermark and no fingerprint.
job = client.accept_signing_job(
body={"asset_id": upload["asset_id"], "watermark": False, "fingerprint": False},
headers={"Idempotency-Key": idempotency_key()},
)
signed = poll(
lambda: client.get_job(parameters={"job_id": job["job_id"]}),
sign_job_terminal,
timeout=300,
)
print(signed["state"], signed["recipe_id"])
sign_job_terminal is the completion test for job_... Sign snapshots. It fails closed on
identifier or readiness drift: a completed Sign result must carry a ready vfy_... evidence ID.
Use verification_job_terminal for vjob_... public Verify jobs; a vjob_... is only a pollable
job and never immutable evidence.
Choose what a signing job adds
Every job signs with C2PA. Two flags choose what else it does (an omitted flag means false):
| You want | Request fields |
|---|---|
| C2PA Content Credentials only | watermark: false, fingerprint: false |
| An invisible watermark, then C2PA | watermark: true |
| A watermark and a fingerprint, then C2PA | watermark: true, fingerprint: true |
| A fingerprint, then C2PA | fingerprint: true |
Send both flags every time (only an older key with legacy defaults may leave them out). The job
reports the exact recipe_id it ran. Integrations that already send a recipe_id keep working; a
flag sent with it must agree with that recipe or the request is refused.
Examples
1. Add an invisible watermark and fingerprints
watermarked = client.accept_signing_job(
body={"asset_id": asset_id, "watermark": True},
headers={"Idempotency-Key": idempotency_key()},
)
fingerprinted = client.accept_signing_job(
body={"asset_id": asset_id, "watermark": True, "fingerprint": True},
headers={"Idempotency-Key": idempotency_key()},
)
Each job is a separate signing of the asset. A watermark request accepts only media types that
the deployment supports for watermarking; client.get_capabilities() lists them.
2. Declare AI generation (EU AI Act Article 50, SB 942)
To declare that a generative model produced the file, add ai_declaration and keep
"watermark": False. A watermark together with an AI declaration is refused with
JOB_POLICY_REJECTED and detail_code AI_DECLARATION_REQUIRES_SIGN_ONLY.
job = client.accept_signing_job(
body={
"asset_id": asset_id,
"watermark": False,
"fingerprint": True,
"ai_declaration": {
"software_agent_name": "Studio Nova Imagine",
"software_agent_version": "2.0",
"digital_source_type": "trainedAlgorithmicMedia",
},
},
headers={"Idempotency-Key": idempotency_key()},
)
3. Download the signed file with integrity checks
result = signed["result"]
if result is None:
raise RuntimeError("the job did not publish an asset")
grant = client.create_asset_download(parameters={"asset_id": result["asset_id"]})
client.download_file(
grant["url"],
Path("photo.signed.jpg"),
byte_length=grant["byte_length"],
sha256=grant["sha256"],
headers=grant["headers"],
)
The download URL is a short-lived bearer capability: keep it out of logs. The file is written to a temporary name and moved into place only after its length and SHA-256 match.
4. Check a file on a public verify page (no API key)
from originalpictures import verify_public
result = verify_public("photo.jpg", page="studio-nova")
print(result["content_credentials"]["status"], result["made_with"]["answer"])
print(result["statement"])
verify_public states what the file declares about its origin and whether the page owner's
system has a record of it. Show the result's statement with it. A file may be at most 25 MB, and
a running check is polled every 2 seconds. verify_public_async is the asynchronous form,
fetch_verify_page("studio-nova") reads the page's public settings, and
Client.verify_public(slug, file) does the same on an existing client. Each check is billed to the
page owner.
5. Verify a webhook delivery
from originalpictures import RequestValidationError, verify_webhook
def handle(raw_body: bytes, headers: dict[str, str], secret: str) -> bool:
try:
verify_webhook(
raw_body,
secret=secret, # the whsec_... value shown once when the endpoint was created
event_id=headers["webhook-id"],
timestamp=headers["webhook-timestamp"],
signature=headers["webhook-signature"],
)
except RequestValidationError:
return False
return True
Pass the exact raw request bytes: parsing and re-serializing the JSON first invalidates the HMAC. Several space-delimited signatures are accepted during secret rotation.
6. Async client and error handling
import asyncio
from originalpictures import AsyncClient, RateLimitError
async def recent_assets() -> None:
async with AsyncClient(
base_url="https://api.originalpictures.com", api_key="opk_..."
) as client:
try:
page = await client.list_assets(parameters={"limit": 20})
except RateLimitError as error:
print("retry later", error.code, error.request_id)
return
for item in page["items"]:
print(item["asset_id"])
asyncio.run(recent_assets())
Configuration
| Setting | Where | Default |
|---|---|---|
| API origin | Client(base_url=...) |
none; use https://api.originalpictures.com |
| API key | Client(api_key=...) |
none; read it from your secret store |
| Request timeout | Client(timeout=...) (seconds) |
30.0 |
| JSON response bound | Client(max_response_bytes=...) |
DEFAULT_MAX_RESPONSE_BYTES (16 MiB) |
| Custom HTTP client | Client(http_client=httpx.Client) |
a private httpx.Client |
| Public check origin | verify_public(..., api_base=...) |
https://api.originalpictures.com |
The SDK reads no environment variables itself; the examples use ORIGINALPICTURES_API_KEY by
convention, the same name the CLI and MCP server read. Do not put API keys in command arguments or
logs. API base URLs may include a path prefix, which is preserved; query strings and fragments are
rejected locally before any request or credential is sent.
poll_async cancels an in-flight coroutine at its wall deadline. poll runs each fetch in one of at
most eight daemon workers and never queues work; when all are busy, new polls fail with
PollingCapacityError. Give callbacks a finite I/O timeout no longer than the poll budget.
FAQ
Is this C2PA?
Yes. Signing writes standard C2PA manifests (Content Credentials) that any C2PA-aware validator,
such as c2patool, can read. Original Pictures is listed on the C2PA Conforming Products List as a
generator product under the C2PA Conformance Program 0.2 (C2PA Technical Specification 2.4).
Is Original Pictures audited? Original Pictures Technologies, Inc. holds ISO/IEC 27001:2022 certification for its information security management system (GCAI, certificate GCAI-ISMS-PD91) and ISO/IEC 42001:2023 certification for its AI management system (TNV). It has a SOC 2 Type I report from Percilchofe CPA LLC as of 19 October 2026, and runs a GDPR compliance program. The certifications cover management systems, not this package. See Security and compliance.
Does it store my media?
Files you upload are processed on Original Pictures' private servers, encrypted in transit and at
rest, in isolated processing. Published outputs are kept for the retention your API key selects
(1h, 24h, 72h or retain; new accounts default to 72 hours). Operator views show metadata
only, and emergency access is audited. This is private server processing, not end-to-end
encryption. See Retention and pricing.
What formats are supported?
Embedded C2PA for JPEG, PNG, WebP, TIFF, AVIF, WAV, MP3, AAC (.m4a) and MP4 (H.264/AAC).
Sign-only embedding for GIF, JPEG XL, SVG, DNG, HEIC/HEIF, QuickTime, AVI and FLAC. A sidecar
.c2pa manifest for PDF, EPUB, DOCX, text, fonts and ML model files. Watermarks and fingerprints
apply only to media types that can be decoded. client.get_capabilities() returns the live list;
see Supported media.
Does a watermark replace C2PA? No. C2PA records declared origin in metadata; the watermark and fingerprints help find your record again when that metadata has been stripped. A file declared as AI-generated is signed without a watermark.
Does this make me meet the EU AI Act or SB 942? It produces machine-readable marking of the kind those laws describe. Whether your use meets an obligation is a legal question for your counsel; using the SDK does not by itself satisfy any law.
How much does it cost? See pricing. Signing jobs, watermarks, fingerprints and verifications are billable operations.
How do I get an API key? Sign in to the console and create one under API keys. Give each integration its own key with only the scopes it needs.
Links
- Documentation: https://docs.originalpictures.com/
- Python SDK guide: https://docs.originalpictures.com/sdk/python/
- API reference: https://docs.originalpictures.com/api/
- Website: https://originalpictures.com
- Related packages:
@originalpictures/sdk(TypeScript),originalpictures-cli,originalpictures-mcp - Support: hello@originalpictures.com
Licence
Licensed under the Apache License, Version 2.0; the
full text is in the LICENSE file shipped with this package. Copyright © 2026 Original Pictures
Technologies, Inc. The licence grants no rights to the Original Pictures name or logo.
Metadata
Release files for originalpictures-sdk 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| originalpictures_sdk-1.0.0.tar.gz | 79.0 kB | With provenance | Details for originalpictures_sdk-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| originalpictures_sdk-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for originalpictures_sdk-1.0.0-py3-none-any.whl |
Total release size: 162.7 kB
Release files / originalpictures_sdk-1.0.0.tar.gz
| Download URL | originalpictures_sdk-1.0.0.tar.gz |
|---|---|
| Size | 79.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2c5b50b6e8bbb5e16a48bd87e62935ecff2a5b188e1479bbf1832a9caa3a8acd
|
|
BLAKE2b-256 checksum How to use checksums |
eebbbc41a60b32e89cfc313c8c525f8ed74d0114bdca4ea3e264c368e011c42a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logRelease files / originalpictures_sdk-1.0.0-py3-none-any.whl
| Download URL | originalpictures_sdk-1.0.0-py3-none-any.whl |
|---|---|
| Size | 83.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
76094fa78a0eae4ecb4fcbe4b6b7005c2c75f073bb0d29ea06c6218cc7819b9e
|
|
BLAKE2b-256 checksum How to use checksums |
9eb1570f45b3575c24d14574b18ee5bd5e1377b9d74e3eb3351ccf6121dd2908
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logSecurity for originalpictures-sdk 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for originalpictures_sdk-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.