Skip to content

Sign

llms.txtAsk agent

Signing takes three steps: create an upload, accept a job with POST /v1/sign, and poll the job. The API never accepts media bytes in a JSON request.

POST /v1/uploads declares the exact bytes you will send:

{
"byte_length": 204800,
"sha256": "<64 lowercase hex characters>",
"declared_media_type": "image/jpeg",
"mode": "single",
"filename": "photo.jpg"
}

declared_media_type must be one of image/jpeg, image/png, image/webp, image/tiff, image/avif, audio/wav, audio/mpeg, audio/mp4, or video/mp4 for the embedded lane, one of the sign-only types image/gif, image/jxl, image/svg+xml, image/x-adobe-dng, image/heic, image/heif, image/heic-sequence, image/heif-sequence, video/quicktime, video/x-msvideo or audio/flac, which are signed only with rcp_system_structural_sign_only, or one of the sidecar media types, among them image/x-tiff-based, image/x-riff-based, video/x-bmff-based, video/x-riff-based, audio/aac, audio/MPA and audio/x-riff-based, whose manifest is delivered beside the file. Check GET /v1/capabilities for current limits and account availability. TIFF and AVIF accept only the image scopes listed in the introduction. Use "mode": "multipart" with a parts list for large files.

MP4 (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/HDR, HE-AAC and fragmented MP4 are refused with MEDIA_CODEC_UNSUPPORTED; phone recordings are often HEVC, so re-export them as H.264/AAC first.

The 201 response contains upload_id, the parent asset_id (ast_…), expires_at, and, for single-part uploads, an upload object with method (PUT), url, and headers.

PUT the file to upload.url and send exactly the returned headers. Do not send your Authorization header to this URL: it is a storage URL, not an API URL.

POST /v1/uploads/{upload_id}/complete needs an Idempotency-Key header. The API checks the stored bytes against the declared length and SHA-256. It then returns the immutable asset_object_id (aob_…). A mismatch fails with UPLOAD_CHECKSUM_MISMATCH or UPLOAD_SIZE_MISMATCH.

POST /v1/sign:

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\"}"
Field Required Description
asset_id yes The ast_… asset whose upload you completed.
recipe_id yes* A recipe from GET /v1/recipes; see Choose a mode.
creator_id no An enrolled creator identity to attribute in the content credentials, if you use them.
watermark_algorithm no The invisible watermark to embed. Only valid with a watermark recipe; see below.
watermark_strength no subtle, balanced or robust. Only valid with a watermark recipe; see below.
fingerprint_algorithms no The fingerprints kept as server-side recovery signals. Omit for the default; see below.
ai_declaration no Declares that a generative model produced the asset. Only valid with a sign-only recipe.
mode no express waits for the result; see Express. Omit for the asynchronous job.

* recipe_id may be omitted only when the API key’s signing profile names a default recipe.

The 202 response is a job snapshot with job_id (job_…) and state.

POST /v1/sign/batch takes up to 100 of the same request bodies as {"items": [...]} and needs the same sign:write scope. Each item is admitted exactly as its own POST /v1/sign would be, with the same quota and errors. One item’s refusal does not affect the others, so a 202 answer can still contain refused items.

  • The response lists one result per item, in request order: index, status (202 or the error status), job for an admitted item and error for a refused one.
  • Item i is admitted under the idempotency key <Idempotency-Key>.b<i>. Replaying the same batch returns the same jobs. The batch key can be at most 196 characters, which leaves room for that suffix.
  • A key with a per-minute rate limit spends one request per item, all or nothing. If the limit cannot cover the whole batch, the batch gets 429 with Retry-After and no item is admitted. A batch larger than the key’s burst allowance can never fit, so it gets 422 with the largest allowed size in field_details.items. At 60 requests per minute that size is 11.

Three signing modes cover the decoded media types (images, audio and video):

Mode recipe_id What the job does
Sign rcp_system_sign Writes and signs the C2PA manifest. No watermark and no fingerprint.
Sign + watermark rcp_system_watermark_then_sign_unfingerprinted Embeds an invisible watermark, then the manifest. No fingerprint.
Sign + watermark + fingerprint rcp_system_watermark_then_sign A watermark, server-side recovery fingerprints, then the manifest.

rcp_system_sign_only still signs with fingerprints and no watermark, and rcp_system_watermark_only still watermarks without a manifest; existing integrations keep working unchanged.

What a mode without fingerprints gives up: nothing is kept that could find your published original from a copy whose manifest was removed, except the watermark in “Sign + watermark”. An image signed with “Sign” carries no ISCC soft binding in its manifest; an image watermarked with a registered TrustMark variant keeps that watermark’s soft binding. Both modes refuse fingerprint_algorithms with HTTP 422 JOB_RECIPE_INVALID, and “Sign” refuses watermark_algorithm and watermark_strength the same way.

Every field in this request can also be defaulted, and some restricted, per API key; see Signing profile. A field the request names always wins over the key’s default. A request the key’s profile does not allow is refused with HTTP 403 API_KEY_PROFILE_FORBIDS, naming the field.

For JPEG, PNG, WebP, TIFF and AVIF images, "mode": "express" runs Sign + watermark + fingerprint on the express lane (rcp_system_express_watermark_then_sign): the same watermark, fingerprints and C2PA manifest, with the job’s steps run back to back instead of queued one by one. The request waits for the result:

  • 200: the job finished within the wait. The body is the job resource plus download, the same short-lived delivery POST /v1/assets/{asset_id}/download mints. download.url is a bearer URL; keep it out of logs.
  • 202: the job had not finished yet. The body is the usual job snapshot; poll it or wait for the webhook, exactly as for any job.

mode applies to rcp_system_watermark_then_sign and to the express recipe itself. Any other recipe or media type runs the ordinary pipeline and answers 202. Webhooks are delivered either way, and the job, receipt, verification and Library entry are the same as for any Sign job. Naming rcp_system_express_watermark_then_sign without mode runs the lane and answers 202; a key’s signing profile can make it the default recipe.

The watermark recipes are rcp_system_watermark_then_sign, which embeds an invisible watermark and then writes the C2PA manifest, and rcp_system_watermark_only, which embeds the watermark without a manifest. Both accept an optional watermark_algorithm:

  • Omit it and the job uses the default for the asset’s media type, listed in the table below. A later change of default never changes a job that already exists.
  • Name the recommended algorithm to opt in to the one that ranks best in our watermark benchmark. Where it is already the default, naming it runs the same algorithm as omitting the field.
  • Any other value must be one of the algorithms listed for that media type in GET /v1/capabilities, under media[].watermark_algorithms. A sign-only recipe refuses the field.
{
"asset_id": "ast_…",
"recipe_id": "rcp_system_watermark_then_sign",
"watermark_algorithm": "pixelseal"
}

The defaults are summarised below. Every algorithm, which media types take it, its C2PA soft-binding status and its strength levels are on Watermarks. The table is generated from the capability snapshot the API is built with; a deployment can enable fewer, so GET /v1/capabilities stays the authority at run time.

Modality Default (field omitted) Recommended Selectable
audio audioseal-mm02-4 audioseal-mm02-4 audioseal, audioseal-mm02-4
image pixelseal pixelseal pixelseal, trustmark-b, trustmark-c, trustmark-p, trustmark-q, videoseal-image
video pixelseal-video pixelseal-video pixelseal-video, videoseal

Sign-only and sidecar media carry no watermark.

Each watermark algorithm is qualified at one strength or a short ladder of strengths. By default a job embeds at the lowest rung and climbs only when the watermark would not be detected in the output. watermark_strength narrows that ladder for one request:

  • subtle embeds at the lowest qualified strength only. If the watermark would not be detected there, the job fails rather than embedding more strongly.
  • balanced is the default: exactly what omitting the field does.
  • robust allows the full qualified ladder, up to the strongest rung.

No level ever uses a strength outside the algorithm’s qualified ladder. For every algorithm today the default already allows the full ladder, so balanced and robust run the same rungs; they differ only for an algorithm whose default is capped below its qualified maximum. For a single-strength algorithm all three levels are the same.

{
"asset_id": "ast_…",
"recipe_id": "rcp_system_watermark_then_sign_unfingerprinted",
"watermark_strength": "subtle"
}

GET /v1/capabilities lists the exact strengths each level allows for each algorithm, under media[].watermark_algorithms[].strength_levels. The level applies to the algorithm the job runs: the one you name, or the media type’s default. A job keeps the strengths it was admitted with. A sign-only recipe refuses the field with HTTP 422 JOB_RECIPE_INVALID; a media type without a watermark (for example audio/mp4) refuses it with JOB_POLICY_REJECTED.

Every recipe that decodes the asset computes perceptual fingerprints of the published bytes. They are kept on our servers so verification can find your published original from a copy whose manifest was removed. The optional fingerprint_algorithms list chooses which:

  • Omit it and the job computes the default set for the asset’s media type, listed below. A later change of default never changes a job that already exists.
  • A list replaces the default set. Name each fingerprint once, at least one, and only those GET /v1/capabilities lists for that media type under media[].fingerprint_algorithms. A refusal is HTTP 422 with FINGERPRINT_ALGORITHM_UNSUPPORTED.
  • ISCC Image-Code is always added for images, because it is the soft binding the manifest carries. No other fingerprint enters the manifest or any public response.
  • A set that differs from the default adds a fingerprint line to the job’s price, on the watermark schedule; see Retention and pricing.
  • The “Sign” and “Sign + watermark” modes compute no fingerprint and refuse the field.
{
"asset_id": "ast_…",
"recipe_id": "rcp_system_sign_only",
"fingerprint_algorithms": ["dinov2", "pdq"]
}

The defaults are summarised below. Every fingerprint, which media types keep it and what each survives are on Fingerprints. The table is generated from the same capability snapshot as the watermark table; GET /v1/capabilities stays the authority at run time.

Modality Default (field omitted) Always added Selectable
audio chromaprint none chromaprint, audfprint, iscc-audio
image iscc-image, dinov2 iscc-image iscc-image, dinov2, pdq, dinov2-lsh256
video tmk-pdqf none tmk-pdqf, vpdq

Poll GET /v1/jobs/{job_id} until state is terminal: completed, failed, rejected, canceled, or expired. Non-terminal states are accepted, queued, running, and cancel_requested. Wait at least 2 seconds between polls, backing off for longer jobs.

A completed job has a result with asset_id, publication_id, receipt_id, and verification_id. That last value is ready verification evidence (vfy_…), not a job ID. A failed job carries an error_code from the reason codes.

To stop a job, send a cancellation request with POST /v1/jobs/{job_id}/cancel. It never publishes partial output.