Skip to content

Authentication

llms.txtAsk agent

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

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.

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_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 Requests
Retry-After: 2
RateLimit-Policy: "per-key";q=120;w=60

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

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.

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.

Rotation is a bounded two-step process that avoids downtime:

  1. POST /v1/api-keys/{api_key_id}/rotate issues a successor key and shows its secret once.
  2. 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/cancel revokes 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.

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-logs lists recent requests, newest first. Filter by api_key_id, status_class (2xx, 3xx, 4xx, 5xx), and route, and page with cursor and limit (1 to 200, default 50).
  • GET /v1/api-request-logs/summary returns counts by status class, the number of refused (403 and 429) requests, and p50 and p95 durations, over a window of 1h, 24h (the default), or 7d.

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.

  • 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_KEY for the key and OP_API_BASE_URL for the API origin.
  • Never send the key to the upload or download URLs the API issues. Those are storage URLs.