Skip to content

Downloads and sidecar manifests

llms.txtAsk agent

A finished signing job publishes the signed asset. You fetch it with a short-lived download URL, check the bytes, and save it. For most formats the C2PA manifest is inside the file. For the sidecar media types it is a separate .c2pa file that you download the same way and keep next to the asset.

POST /v1/assets/{asset_id}/download needs the assets:read scope and the job’s asset_id. The response describes one object:

Field Description
method, url The request to make for the bytes. The URL is short-lived.
headers Send exactly these headers, and nothing else, with that request.
expires_at When the URL stops working. Ask for a new one after that.
media_type The media type of the published bytes.
byte_length, sha256 The exact length and lowercase SHA-256 of the published bytes.
storage_version_sha256 An identifier of the stored version, for support requests.
filename The name to save it under: the asset id plus the format extension.
sidecar_manifest Present only when the manifest travels beside the asset.

The URL is a storage URL, not an API URL. Do not send your Authorization header to it.

After the download, compare the length and SHA-256 of what you received with byte_length and sha256. Discard the file if either differs, and request a new download.

Terminal window
curl -sS -X POST "$OP_API_BASE_URL/v1/assets/$ASSET_ID/download" \
-H "Authorization: Bearer $OP_API_KEY" > download.json
HEADERS=()
while IFS= read -r header; do HEADERS+=(-H "$header"); done \
< <(jq -r '.headers | to_entries[] | "\(.key): \(.value)"' download.json)
FILENAME=$(jq -r .filename download.json)
curl -sS "${HEADERS[@]}" -o "$FILENAME" "$(jq -r .url download.json)"
test "$(shasum -a 256 "$FILENAME" | cut -d' ' -f1)" = "$(jq -r .sha256 download.json)"

When the response has sidecar_manifest, the asset bytes are exactly the bytes you uploaded and its C2PA manifest is a separate application/c2pa object. sidecar_manifest has the same fields as the asset download: method, url, headers, expires_at, media_type, byte_length, sha256, storage_version_sha256 and filename. Its filename is the asset’s own filename plus .c2pa.

Terminal window
if jq -e .sidecar_manifest download.json > /dev/null; then
MANIFEST=$(jq -r .sidecar_manifest.filename download.json)
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 "$MANIFEST" "$(jq -r .sidecar_manifest.url download.json)"
test "$(shasum -a 256 "$MANIFEST" | cut -d' ' -f1)" = "$(jq -r .sidecar_manifest.sha256 download.json)"
fi

Keep the two files together and keep both names. The manifest proves nothing without the exact bytes it binds: its hard binding covers the whole asset file, so any change to the asset breaks it, and a manifest separated from its asset cannot be checked at all.

To verify a sidecar asset this service signed, submit the asset’s object id to POST /v1/verifications as for any other format. Send only the object id. The API finds the stored manifest itself.

To verify a pair that another product signed, upload both files and send both object ids: object_id for the asset and manifest_object_id for the .c2pa. See Verify a pair another product signed.

GET /v1/capabilities is the authority for what a deployment accepts now. Each sidecar family signs with its own sign-only recipe, which appears in recipes[] while at least one of its formats is enabled. Sidecar formats carry no watermark. The reviewed list is in the introduction.