Original Pictures agent install guide
Everything an AI coding agent needs to integrate the Original Pictures API: authenticate, make a first call, and run the full upload, sign, download and verify flow. Original Pictures signs media with C2PA manifests (Content Credentials), can add an invisible watermark, and verifies the result, through a REST API. It is the same text the console’s “Copy agent prompt” hands an assistant.
Before you start
Section titled “Before you start”- Read the documentation index at https://docs.originalpictures.com/llms.txt first, then the pages it links for the steps you implement. This guide is a starting point written from the contract; where a page and this guide disagree, report the specific difference and follow the page.
- Inspect the project before changing it: its language and framework, where server code runs, how users or tenants are modelled, and where secrets and background jobs live. If there is no application yet, create the smallest server that can hold the key and run the flow.
- Keep every API call on the server. When the people using this project bring their own Original Pictures key, collect the complete key in one password field labelled “API key”, store it server-side (for example an HTTP-only cookie or the secret store), and never put it in localStorage, client bundles, logs or responses.
- Store each
job_id,upload_idandverification_idwith the user or tenant that created it, and check that ownership before returning status, downloads or evidence. - Use links from the documentation; never invent a documentation URL, an endpoint or a field. If something cannot be verified, say so and continue with the work that does not depend on it.
OP_API_KEYis a server-only credential read from the environment. Never inline it, never ask for it in chat, and never print, log, commit, or ship it to a browser or mobile client.- Send it as
Authorization: Bearer $OP_API_KEYon every request tohttps://dev-api.originalpictures.com, and on nothing else: the upload and download URLs the API returns take exactly the headers returned with them. - Call the API from server code with plain HTTPS in this project’s own language. No SDK, CLI or MCP server is published yet, so do not install one.
GET /v1/capabilitiesis the authority for what this deployment accepts now. The lists below are what the product has reviewed; a deployment may enable fewer.- Use only the endpoints, fields, recipes and values in this guide. Do not invent an endpoint, a scope, a format, a limit or a price.
- Do not describe signed output as approved, endorsed or accredited by any standards body.
1. Set the API key
Section titled “1. Set the API key”The person creates a key in the Original Pictures console under API keys; its secret is shown once. Ask them to set it in the server environment themselves, then confirm it is present with test -n "$OP_API_KEY" without printing it.
# Server-only. Never commit it, log it, or ship it to a browser.export OP_API_KEY='paste-your-key-here'export OP_API_BASE_URL='https://dev-api.originalpictures.com'2. Make a first call
Section titled “2. Make a first call”Call GET /v1/capabilities. It needs the capabilities:read scope. A 200 proves the key and the origin. A 401 means the key is missing, wrong, expired or revoked. A 403 means the key lacks the scope: it is not a broken key. If it returns 403, make the first call one the key’s scopes allow instead, for example GET /v1/me (scope identity:read) or GET /v1/recipes (scope jobs:read).
curl --fail-with-body https://dev-api.originalpictures.com/v1/capabilities \ -H "Authorization: Bearer $OP_API_KEY"3. Check what this deployment accepts
Section titled “3. Check what this deployment accepts”Read GET /v1/capabilities (scope capabilities:read) before the first upload. Submit a format only when its media[].availability.available is true, respect its media[].upload.maximum_bytes, and choose a recipe from recipes[] whose availability.available is true and whose active.supported_media_types includes the format. The reviewed formats are:
- Embedded manifest, decoded (watermark recipes where a watermark is offered):
audio/mp4,audio/mpeg,audio/wav,image/avif,image/jpeg,image/png,image/tiff,image/webp,video/mp4. - Embedded manifest, signed without decoding (sign-only):
audio/flac,image/gif,image/heic,image/heic-sequence,image/heif,image/heif-sequence,image/jxl,image/svg+xml,image/x-adobe-dng,video/quicktime,video/x-msvideo. - Sidecar manifest, delivered beside the file as
.c2pa(sign-only):application/epub+zip,application/onnx,application/oxps,application/pdf,application/vnd.oasis.opendocument.text,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/x-jax,application/x-keras,application/x-mlnet,application/x-mxnet,application/x-npy,application/x-openvino-ir+xml,application/x-openvino-weights,application/x-pickle,application/x-protobuf,application/x-pytorch,application/x-tensorflow,application/x-tensorflow-savedmodel,application/x-zip-based,application/xhtml+xml,application/xml,audio/MPA,audio/aac,audio/x-riff-based,font/otf,image/x-riff-based,image/x-tiff-based,text/html,text/plain,text/xml,video/x-bmff-based,video/x-riff-based.
video/mp4 accepts one H.264 video track (8-bit 4:2:0) and at most one AAC-LC audio track, in a non-fragmented file with no other tracks. HEVC, 10-bit or HDR video, HE-AAC and fragmented MP4 fail with MEDIA_CODEC_UNSUPPORTED; re-encode to H.264/AAC first.
Recipes by lane. Embedded: rcp_system_express_watermark_then_sign, rcp_system_sign, rcp_system_sign_only, rcp_system_watermark_only, rcp_system_watermark_then_sign, rcp_system_watermark_then_sign_unfingerprinted. Structural: rcp_system_structural_sign_only. Each sidecar family has its own sign-only recipe:
rcp_private_sidecar_audio_sign_only:audio/MPA,audio/aac,audio/x-riff-basedrcp_private_sidecar_document_sign_only:application/epub+zip,application/oxps,application/pdf,application/vnd.oasis.opendocument.text,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/x-zip-basedrcp_private_sidecar_font_sign_only:font/otfrcp_private_sidecar_image_sign_only:image/x-riff-based,image/x-tiff-basedrcp_private_sidecar_model_sign_only:application/onnx,application/x-jax,application/x-keras,application/x-mlnet,application/x-mxnet,application/x-npy,application/x-openvino-ir+xml,application/x-openvino-weights,application/x-pickle,application/x-protobuf,application/x-pytorch,application/x-tensorflow,application/x-tensorflow-savedmodelrcp_private_sidecar_text_sign_only:application/xhtml+xml,application/xml,text/html,text/plain,text/xmlrcp_private_sidecar_video_sign_only:video/x-bmff-based,video/x-riff-based
GET /v1/capabilities lists every one of these recipes in recipes[]; a sidecar recipe appears while at least one of its formats is enabled, with exactly those formats in active.supported_media_types.
audio/mp4 has no watermark, so only the sign-only recipe applies.
4. Upload the file
Section titled “4. Upload the file”Create an upload with POST /v1/uploads (scope uploads:write). Required fields: byte_length, declared_media_type, mode, sha256; sha256 is the lowercase hex SHA-256 of the exact bytes and mode is single (or multipart with parts for large files). Keep upload_id and asset_id from the response.
Send the bytes with the upload.method to upload.url, with exactly upload.headers and without the API key. Then call POST /v1/uploads/{upload_id}/complete (scope uploads:write) with an Idempotency-Key header and {} as the body (multipart: parts with each part’s ETag). The response’s asset_object_id is the stored object.
curl --fail-with-body -X POST "$OP_API_BASE_URL/v1/uploads" \ -H "Authorization: Bearer $OP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"byte_length": <bytes>, "sha256": "<sha256>", "declared_media_type": "<media type>", "mode": "single"}'# PUT the file to .upload.url with exactly .upload.headers (no Authorization), then:curl --fail-with-body -X POST "$OP_API_BASE_URL/v1/uploads/$UPLOAD_ID/complete" \ -H "Authorization: Bearer $OP_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{}'5. Sign it
Section titled “5. Sign it”Start a job with POST /v1/sign (scope sign:write), an Idempotency-Key header, and asset_id plus recipe_id. rcp_system_sign_only writes a C2PA manifest; rcp_system_watermark_then_sign adds an invisible watermark first; rcp_system_watermark_only watermarks without a manifest. The response is 202 with job_id and state.
watermark_algorithm is optional and only valid with a watermark recipe. Omit it to get the default. The recommended algorithm is the watermark benchmark’s best-ranked one; where it is not the default it runs only when you name it. By modality:
- audio: default and recommended
audioseal-mm02-4; selectable:audioseal,audioseal-mm02-4. - image: default and recommended
pixelseal; selectable:pixelseal,trustmark-b,trustmark-c,trustmark-p,trustmark-q,videoseal-image. - video: default and recommended
pixelseal-video; selectable:pixelseal-video,videoseal.
fingerprint_algorithms is optional too: the fingerprints kept on the server to recover a copy whose manifest was removed. Omit it for the default set; a list replaces it, and the always-added ones stay. Only a changed set adds a price line. By modality:
- audio: default
chromaprint; selectable:chromaprint,audfprint,iscc-audio. - image: default
iscc-image,dinov2, alwaysiscc-image; selectable:iscc-image,dinov2,pdq,dinov2-lsh256. - video: default
tmk-pdqf; selectable:tmk-pdqf,vpdq.
curl --fail-with-body -X POST "$OP_API_BASE_URL/v1/sign" \ -H "Authorization: Bearer $OP_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"asset_id": "<asset_id>", "recipe_id": "<recipe_id from step 3>"}'6. Wait for the job
Section titled “6. Wait for the job”Poll GET /v1/jobs/{job_id} (scope jobs:read) until state is terminal: completed, failed, rejected, canceled, expired. Back off between polls: start at 1 second, double each time up to 30 seconds, and add jitter. On failed or rejected, report error_code. A completed job’s result carries receipt_id, publication_id and verification_id. Webhooks (below) can replace polling.
7. Download the result
Section titled “7. Download the result”Call POST /v1/assets/{asset_id}/download (scope assets:read) with the job’s asset_id. GET url with exactly headers (no API key) before expires_at; ask again if it has expired. Check the bytes against sha256 and byte_length and save them as filename.
When the response has sidecar_manifest, the manifest travels beside the file: fetch sidecar_manifest.url the same way, check its sha256 and byte_length, and save it as sidecar_manifest.filename (the file’s name plus .c2pa) next to the file. Keep the two together: the manifest proves nothing without the bytes it binds.
8. Verify it
Section titled “8. Verify it”Read GET /v1/receipts/{receipt_id} (scope assets:read) for the job’s receipt_id; evidence.output.object_id is the signed object. Submit it to POST /v1/verifications (scope verify:write) with an Idempotency-Key header and {"object_id": "<object_id>"}. Poll GET /v1/verifications/{verification_id} (scope recovery:read) with the same backoff until state is completed or failed; a completed verification has evidence_id and result. For a sidecar file, send only the object id: the API finds the stored manifest itself.
The job’s own verification_id is readable at once with GET /v1/verification-evidence/{verification_id} (scope recovery:read).
9. Receive webhooks
Section titled “9. Receive webhooks”Optional. Register an HTTPS endpoint with POST /v1/webhook-endpoints (scope webhooks:write), an Idempotency-Key header, url, events and "event_catalog_version": "op.customer-events.v1". The response returns the signing secret once; store it like the API key. Events:
asset.signed: A signing job finished and the asset now carries a verifiable credential.asset.watermarked: A watermarking job finished and the processed asset is available.
Deliveries follow Standard Webhooks. Verify every one before acting on it:
- Read the raw body bytes and the
webhook-id,webhook-timestampandwebhook-signatureheaders. Do not parse and re-serialize the JSON first. - Reject the delivery if the timestamp is more than 300 seconds from now.
- The key is the base64-decoded part of the secret after
whsec_. Compute HMAC-SHA256 over{webhook-id}.{webhook-timestamp}.{raw request body}and base64-encode it. webhook-signatureholds one or morev1,<base64 digest>values, space-delimited during secret rotation; accept any match. Compare in constant time.- Deduplicate on
webhook-idand answer 2xx quickly; do the work after responding.
10. Handle errors and retries
Section titled “10. Handle errors and retries”API errors are JSON with code, documentation_url, field_details, job_reference, message, request_id, retryable. Authentication and scope failures (401, 403) carry only detail. code is a stable reason code: branch on it, never on message. Log request_id, never the key.
- Retry only when
retryableis true, or on 429 or 503. When the response has aRetry-Afterheader, wait at least that many seconds; otherwise back off exponentially with jitter. - Every write that takes an
Idempotency-Keyheader needs a fresh UUID per logical operation. Reuse the same key when retrying that operation, so a retry never starts a second job. - 401: fix the key. 403: the key lacks the scope. 409: the resource is in a conflicting state. 422: the request does not match the contract.
- A key can carry its own restrictions, set by the person in the console. A key with a rate limit that sends too fast gets 429
API_KEY_RATE_LIMITEDwithRetry-After(whole seconds) andRateLimit-Policy; the refused request used no capacity, so wait and retry it with the sameIdempotency-Key. A key limited to client networks gets 403API_KEY_IP_NOT_ALLOWEDfrom any other address: that is not retryable, so tell the person which network the calls come from. GET /v1/usage(scopebilling:read) reports usage against its limit or credit balance. Never infer prices or limits from request counts.
Done when
Section titled “Done when”GET /v1/capabilitiesreturns 200.- A test file was uploaded, signed, downloaded (with its
.c2pawhen there is one) and its verification completed. - The key appears nowhere in source code, logs or client bundles.
Report back
Section titled “Report back”- What changed, file by file.
- Which environment variables to set (
OP_API_KEYandOP_API_BASE_URL), without their values. - How to run the flow end to end.
- What was not verified: endpoints or fields you could not confirm in the documentation, and whether any call ran against the live API with a real key.
