Quickstart
This walkthrough signs a JPEG, downloads the signed file (with its .c2pa manifest when the format
uses one), and verifies the result. It needs curl, jq, shasum, and uuidgen.
1. Get an API key
Section titled “1. Get an API key”Create a key in the console under API keys.
Grant it uploads:write, sign:write, jobs:read, assets:read, verify:write, and
recovery:read. The console’s sign and verify preset selects exactly these. The secret
is shown once. See Authentication.
export OP_API_BASE_URL=https://dev-api.originalpictures.comexport OP_API_KEY='opk_…' # paste from the console; do not commit itCheck that the key works before you upload anything. GET /v1/capabilities also lists the formats,
recipes, and watermark algorithms this deployment accepts now:
curl -sS "$OP_API_BASE_URL/v1/capabilities" -H "Authorization: Bearer $OP_API_KEY" > capabilities.jsonjq '[.media[] | select(.availability.available) | .media_type]' capabilities.jsonA 401 means the key is missing, wrong, expired, or revoked. A 403 means it lacks the scope.
2. Create an upload
Section titled “2. Create an upload”FILE=photo.jpgSHA256=$(shasum -a 256 "$FILE" | cut -d' ' -f1)SIZE=$(wc -c < "$FILE" | tr -d ' ')
curl -sS -X POST "$OP_API_BASE_URL/v1/uploads" \ -H "Authorization: Bearer $OP_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"byte_length\": $SIZE, \"sha256\": \"$SHA256\", \"declared_media_type\": \"image/jpeg\", \"mode\": \"single\", \"filename\": \"$FILE\"}" \ > upload.json
UPLOAD_ID=$(jq -r .upload_id upload.json)ASSET_ID=$(jq -r .asset_id upload.json)3. Send the bytes
Section titled “3. Send the bytes”PUT the file to the issued URL with exactly the issued headers, and without your API key:
HEADERS=()while IFS= read -r header; do HEADERS+=(-H "$header"); done \ < <(jq -r '.upload.headers | to_entries[] | "\(.key): \(.value)"' upload.json)
curl -sS -X PUT "${HEADERS[@]}" --upload-file "$FILE" "$(jq -r .upload.url upload.json)"4. Complete the upload
Section titled “4. Complete the upload”curl -sS -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 '{}' > completed.json
ASSET_OBJECT_ID=$(jq -r .asset_object_id completed.json)5. Sign
Section titled “5. Sign”For a JPEG, rcp_system_sign_only writes a C2PA manifest. To add an invisible watermark first,
use rcp_system_watermark_then_sign and, optionally, a watermark_algorithm from
Sign. Omit the field to get the default. For a manifest with no
server-side fingerprint, use rcp_system_sign; see Choose a mode.
curl -sS -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\": \"rcp_system_sign_only\"}" > job.json
JOB_ID=$(jq -r .job_id job.json)6. Wait for the job
Section titled “6. Wait for the job”Poll until the job reaches a terminal state. Start at one second and double the wait up to 30 seconds:
DELAY=1while :; do STATE=$(curl -sS "$OP_API_BASE_URL/v1/jobs/$JOB_ID" -H "Authorization: Bearer $OP_API_KEY" \ | tee job.json | jq -r .state) case "$STATE" in completed|failed|rejected|canceled|expired) break ;; esac sleep "$DELAY" DELAY=$(( DELAY * 2 > 30 ? 30 : DELAY * 2 ))donejq '{state, error_code, result}' job.jsonA completed job’s result gives you the receipt_id, publication_id, and ready verification
evidence (verification_id, which starts with vfy_). On failed or rejected, error_code is
a reason code. Webhooks can replace polling.
7. Download the signed file
Section titled “7. Download the signed file”curl -sS -X POST "$OP_API_BASE_URL/v1/assets/$ASSET_ID/download" \ -H "Authorization: Bearer $OP_API_KEY" > download.json
FILENAME=$(jq -r .filename download.json)DOWNLOAD_HEADERS=()while IFS= read -r header; do DOWNLOAD_HEADERS+=(-H "$header"); done \ < <(jq -r '.headers | to_entries[] | "\(.key): \(.value)"' download.json)curl -sS "${DOWNLOAD_HEADERS[@]}" -o "$FILENAME" "$(jq -r .url download.json)"test "$(shasum -a 256 "$FILENAME" | cut -d' ' -f1)" = "$(jq -r .sha256 download.json)"For a sidecar format the response also has sidecar_manifest.
Download it the same way and save it as sidecar_manifest.filename, next to the file:
if jq -e .sidecar_manifest download.json > /dev/null; then SIDECAR_HEADERS=() while IFS= read -r header; do SIDECAR_HEADERS+=(-H "$header"); done \ < <(jq -r '.sidecar_manifest.headers | to_entries[] | "\(.key): \(.value)"' download.json) curl -sS "${SIDECAR_HEADERS[@]}" -o "$(jq -r .sidecar_manifest.filename download.json)" \ "$(jq -r .sidecar_manifest.url download.json)"fiA JPEG carries its manifest inside the file, so this step does nothing for it. See Downloads and sidecar manifests.
8. Verify the signed object
Section titled “8. Verify the signed object”The receipt names the signed object. Submit it for verification:
curl -sS "$OP_API_BASE_URL/v1/receipts/$(jq -r .result.receipt_id job.json)" \ -H "Authorization: Bearer $OP_API_KEY" > receipt.jsonOUTPUT_OBJECT_ID=$(jq -r .evidence.output.object_id receipt.json)
curl -sS -X POST "$OP_API_BASE_URL/v1/verifications" \ -H "Authorization: Bearer $OP_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d "{\"object_id\": \"$OUTPUT_OBJECT_ID\"}" > verification.json
curl -sS "$OP_API_BASE_URL/v1/verifications/$(jq -r .verification_id verification.json)" \ -H "Authorization: Bearer $OP_API_KEY" | jq .Repeat the last call, with the same backoff, until state is completed or failed. A sidecar
file is verified the same way: send only the object id, and the API finds the stored manifest.
Next steps
Section titled “Next steps”- Sign and Verify explain every field in more depth.
- Errors covers retries,
Idempotency-Key, and429responses. - Webhooks notify you when an asset is published, so you can stop polling.
- The agent install guide is this flow written for an AI coding assistant.
