Authentication
Every /v1/ operation requires a bearer credential:
Authorization: Bearer opk_<lookup>.<secret>The API accepts two credential shapes, and the shape decides how it is checked:
- API keys (
opk_…) are for servers, scripts, and CI. Each key carries an explicit set of scopes. - Console sessions are what the web console uses when you are signed in. You do not need them for server integrations.
A missing, invalid, expired, or revoked credential returns 401. The response never says which
of these applies.
Create a key
Section titled “Create a key”Create keys in the console under API keys, or
call POST /v1/api-keys with the governance:write scope.
The request lists the scopes to grant and can set an optional name, expires_at,
restrictions (see Restrict a key), and signing_profile (see
Signing profile).
The secret is shown exactly once, in the creation response. Store it in a secret manager straight away. The API only ever returns key metadata afterwards.
Scopes
Section titled “Scopes”A key can call an operation only if it holds that operation’s scope. Anything not covered by the
scope policy is denied by default and returns 403. You cannot mint a key with more access than
the credential that creates it.
GET /v1/api-keys/scopes returns the authoritative,
versioned scope vocabulary. The scopes for the core flow are:
| Scope | Grants |
|---|---|
uploads:write |
POST /v1/uploads, …/complete, …/abort |
sign:write |
POST /v1/sign |
jobs:read |
GET /v1/jobs/{job_id}, GET /v1/recipes |
jobs:write |
POST /v1/jobs/{job_id}/cancel |
recipes:read |
GET /v1/recipes/{recipe_id} |
assets:read |
Asset reads, downloads, previews, and GET /v1/receipts/{receipt_id} |
verify:write |
POST /v1/verifications, and creating verification reports and shares |
recovery:read |
GET /v1/verifications/{verification_id}, and verification evidence and share listings |
verifications:read |
GET /v1/verifications (history) |
webhooks:read |
Reading webhook endpoints, the event catalog, and deliveries |
webhooks:write |
Creating, updating, enabling, disabling, and deleting webhook endpoints |
webhooks:rotate |
Rotating webhook signing secrets |
governance:read |
API-key metadata and restrictions, the scope catalog, request logs, and the audit log |
governance:write |
Creating, rotating, restricting, and revoking API keys |
identity:read |
GET /v1/me |
There are also scopes for creators, capture, billing, capabilities, and the overview. Every operation page in the API reference lists the credential it needs.
Restrict a key
Section titled “Restrict a key”A key can carry two optional restrictions on top of its scopes: a request rate limit and a list of client networks it may be used from. A key without restrictions is unrestricted, which is how every key issued before restrictions existed keeps working exactly as before.
Set them when you create the key, in restrictions on
POST /v1/api-keys:
{ "name": "Publishing pipeline", "scopes": ["uploads:write", "sign:write", "jobs:read"], "restrictions": { "rate_limit_per_minute": 120, "allowed_ip_cidrs": ["203.0.113.0/24", "2001:db8:1234::/48"] }}Omit a field to leave that dimension unrestricted. Change them later with
PUT /v1/api-keys/{api_key_id}/restrictions
(governance:write), which replaces both fields and takes the expected_version you last read
from GET /v1/api-keys/{api_key_id}/restrictions
(governance:read). A key that was never restricted is at version 0. A stale version returns
409, so read again before retrying. null clears a dimension. New restrictions apply from the
key’s next request.
A key that creates or restricts other keys cannot give them looser restrictions than its own: the
request returns 403. Console sessions have no such ceiling.
Rate limit
Section titled “Rate limit”rate_limit_per_minute is between 1 and 6000. It is enforced as a steady rate, one request every
60 / limit seconds, with a burst allowance of up to ten seconds’ worth of requests. A request
over the limit is refused before it does any work:
HTTP/1.1 429 Too Many RequestsRetry-After: 2RateLimit-Policy: "per-key";q=120;w=60The body is the usual error envelope with code API_KEY_RATE_LIMITED and
retryable: true. Retry-After is whole seconds. The refused request used no capacity, so wait
that long and retry it with the same Idempotency-Key. Only a key with a rate limit can receive
this response, and RateLimit-Policy is sent only on it.
IP allowlist
Section titled “IP allowlist”allowed_ip_cidrs lists 1 to 32 client networks in CIDR notation, IPv4 or IPv6. A bare address
means that single host. The API refuses a network with host bits set (write 10.0.0.0/8, not
10.0.0.1/8), a /0 network (omit the allowlist instead), and IPv4 written as IPv4-mapped IPv6
(::ffff:…). Networks are stored in a canonical, deduplicated form.
A request from outside the allowlist, or from an address the API could not determine, is refused
with 403 and code API_KEY_IP_NOT_ALLOWED. That is not retryable: call from an allowed network,
or change the allowlist.
An allowlist is only accepted where the deployment can trust the client address it sees. Until
then, a request that asks for one returns 409 with code API_KEY_IP_ALLOWLIST_UNAVAILABLE, and
the restrictions response reports ip_allowlist_available: false. A rate limit works either way.
Signing profile
Section titled “Signing profile”A key that signs can carry a signing profile: defaults for the
POST /v1/sign fields a request omits, and optional allowed sets it refuses anything
outside of. A key without a profile, which is every key issued before profiles existed, sends and
is refused exactly what it always was.
Set it when you create the key, in signing_profile on
POST /v1/api-keys:
{ "name": "Newsroom pipeline", "scopes": ["uploads:write", "sign:write", "jobs:read", "assets:read"], "signing_profile": { "default_recipe": "rcp_system_watermark_then_sign_unfingerprinted", "allowed_recipes": ["rcp_system_sign", "rcp_system_watermark_then_sign_unfingerprinted"], "allowed_media_types": ["image/jpeg", "image/png"], "default_watermark_strength": "subtle" }}Every field is optional; null or an omitted field means no default, or not restricted by that
field:
| Field | What it does |
|---|---|
default_recipe |
The recipe a Sign request runs when it omits recipe_id. |
allowed_recipes |
The only recipes the key may run. |
allowed_media_types |
The only media types the key may sign, judged by the uploaded asset. |
default_watermark_algorithm |
The watermark used when the request omits one, for a file it supports. |
allowed_watermark_algorithms |
The only watermarks the key may run. |
default_watermark_strength |
The strength when the request omits it. |
allowed_watermark_strengths |
The only strength levels the key may run. |
default_fingerprint_algorithms |
The fingerprint set when the request omits it, reduced to those the file offers. |
allowed_fingerprint_algorithms |
The only fingerprints the key may compute. ISCC Image-Code is always allowed. |
A field the request names always wins over the key’s default. The allowed sets are judged on what
the job would actually run: a request that omits watermark_algorithm on a key that allows only
trustmark-q is refused rather than silently given the image default. A refusal is HTTP 403
API_KEY_PROFILE_FORBIDS, with the offending request field in field_details. A default that does
not apply to a file (a PixelSeal default on an audio file) is skipped, and the media type’s own
default is used instead.
Change it with
PUT /v1/api-keys/{api_key_id}/signing-profile
(governance:write), sending the whole profile and the expected_version you last read from
GET /v1/api-keys/{api_key_id}/signing-profile
(governance:read), exactly as for restrictions. An all-null profile clears it. A new version
applies from the key’s next Sign request; a job already admitted keeps the version it was admitted
with, which the job records. A key that creates or configures other keys cannot give them a profile
that allows more than its own.
Rotate a key
Section titled “Rotate a key”Rotation is a bounded two-step process that avoids downtime:
POST /v1/api-keys/{api_key_id}/rotateissues a successor key and shows its secret once.- Deploy the successor, then retire the old key with
POST /v1/api-keys/{api_key_id}/rotation/complete. If you change your mind before deploying,…/rotation/cancelrevokes the undeployed successor.
The successor carries over the outgoing key’s restrictions and signing profile. It also shares the outgoing key’s rate budget, so during the overlap the two keys together get one rate limit, not two.
DELETE /v1/api-keys/{api_key_id} revokes a key
immediately.
Request logs
Section titled “Request logs”The API records metadata about each request made with an API key, so you can see what a key is doing and why a request was refused:
GET /v1/api-request-logslists recent requests, newest first. Filter byapi_key_id,status_class(2xx,3xx,4xx,5xx), androute, and page withcursorandlimit(1 to 200, default 50).GET /v1/api-request-logs/summaryreturns counts by status class, the number of refused (403and429) requests, and p50 and p95 durations, over awindowof1h,24h(the default), or7d.
Both need governance:read. Each row has the time, key, method, the route template (such as
/v1/jobs/{job_id}, never the concrete path), status, duration, reason code, request id, and the
client’s /24 (IPv4) or /48 (IPv6) network. The exact client address appears only on an IP
allowlist refusal, so you can fix the allowlist. Bodies, headers, query strings, and tokens are
never recorded. Requests from console sessions and requests that fail authentication (401) are
not logged.
The log is best effort, not evidence: rows are written in the background and can be dropped under load. They are kept for 30 days.
Credential hygiene
Section titled “Credential hygiene”- Never embed a key in client-side code or a mobile app.
- Never pass a key as a command-line argument or write it to logs. Read it from the environment
or a secret manager. The examples in these docs and the console’s Connect snippets use
OP_API_KEYfor the key andOP_API_BASE_URLfor the API origin. - Never send the key to the upload or download URLs the API issues. Those are storage URLs.
