Skip to content

Quickstart

llms.txtAsk agent

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.

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.

Terminal window
export OP_API_BASE_URL=https://dev-api.originalpictures.com
export OP_API_KEY='opk_…' # paste from the console; do not commit it

Check that the key works before you upload anything. GET /v1/capabilities also lists the formats, recipes, and watermark algorithms this deployment accepts now:

Terminal window
curl -sS "$OP_API_BASE_URL/v1/capabilities" -H "Authorization: Bearer $OP_API_KEY" > capabilities.json
jq '[.media[] | select(.availability.available) | .media_type]' capabilities.json

A 401 means the key is missing, wrong, expired, or revoked. A 403 means it lacks the scope.

Terminal window
FILE=photo.jpg
SHA256=$(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)

PUT the file to the issued URL with exactly the issued headers, and without your API key:

Terminal window
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)"
Terminal window
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)

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.

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

Poll until the job reaches a terminal state. Start at one second and double the wait up to 30 seconds:

Terminal window
DELAY=1
while :; 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 ))
done
jq '{state, error_code, result}' job.json

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

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

Terminal window
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)"
fi

A JPEG carries its manifest inside the file, so this step does nothing for it. See Downloads and sidecar manifests.

The receipt names the signed object. Submit it for verification:

Terminal window
curl -sS "$OP_API_BASE_URL/v1/receipts/$(jq -r .result.receipt_id job.json)" \
-H "Authorization: Bearer $OP_API_KEY" > receipt.json
OUTPUT_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.

  • Sign and Verify explain every field in more depth.
  • Errors covers retries, Idempotency-Key, and 429 responses.
  • 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.