Skip to content

Original Pictures agent install guide

llms.txtAsk agent

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.

  • 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_id and verification_id with 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_KEY is 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_KEY on every request to https://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/capabilities is 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.

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.

Terminal window
# 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'

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).

Terminal window
curl --fail-with-body https://dev-api.originalpictures.com/v1/capabilities \
-H "Authorization: Bearer $OP_API_KEY"

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-based
  • rcp_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-based
  • rcp_private_sidecar_font_sign_only: font/otf
  • rcp_private_sidecar_image_sign_only: image/x-riff-based, image/x-tiff-based
  • rcp_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-savedmodel
  • rcp_private_sidecar_text_sign_only: application/xhtml+xml, application/xml, text/html, text/plain, text/xml
  • rcp_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.

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.

Terminal window
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 '{}'

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, always iscc-image; selectable: iscc-image, dinov2, pdq, dinov2-lsh256.
  • video: default tmk-pdqf; selectable: tmk-pdqf, vpdq.
Terminal window
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>"}'

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.

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.

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).

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-timestamp and webhook-signature headers. 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-signature holds one or more v1,<base64 digest> values, space-delimited during secret rotation; accept any match. Compare in constant time.
  • Deduplicate on webhook-id and answer 2xx quickly; do the work after responding.

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 retryable is true, or on 429 or 503. When the response has a Retry-After header, wait at least that many seconds; otherwise back off exponentially with jitter.
  • Every write that takes an Idempotency-Key header 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_LIMITED with Retry-After (whole seconds) and RateLimit-Policy; the refused request used no capacity, so wait and retry it with the same Idempotency-Key. A key limited to client networks gets 403 API_KEY_IP_NOT_ALLOWED from any other address: that is not retryable, so tell the person which network the calls come from.
  • GET /v1/usage (scope billing:read) reports usage against its limit or credit balance. Never infer prices or limits from request counts.
  • GET /v1/capabilities returns 200.
  • A test file was uploaded, signed, downloaded (with its .c2pa when there is one) and its verification completed.
  • The key appears nowhere in source code, logs or client bundles.
  • What changed, file by file.
  • Which environment variables to set (OP_API_KEY and OP_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.