Your key
Scopes
A key carries scopes and an operation checks its scope before it does anything else. A call never widens the key that made it: ask for an approval you do not hold and it is refused rather than escalated.
- approvals:read
- Read approvals and their recorded basis — scope, jurisdictions, artwork hash, evidence snapshot, decision. Separate from approvals:write because seeing who approved what is a different authority from approving.
- approvals:write
- Grant or withhold an approval within the scope the principal actually holds. Never widens that scope.
- billing:read
- Read plan, usage and meter state.
- billing:write
- Change plan or manage billing configuration.
- counterparties:read
- Read counterparties and the evidence they have supplied.
- counterparties:write
- Invite counterparties and manage their obligations.
- evidence:read
- Read evidence on file and the outstanding requests against counterparties. Separate from evidence:write because reading what a supplier owes is routine and uploading on their behalf is not.
- evidence:write
- Submit evidence against a counterparty obligation.
- findings:read
- Read findings raised by a run.
- findings:write
- Waive or annotate a finding. A blocking finding can only be waived by a principal holding the approval authority for that market.
- manifests:read
- Read packaging manifests and their per-market matrices.
- manifests:write
- Create and amend manifests. A manifest in an approved state cannot be amended without superseding it.
- packs:read
- Read rule packs and the rules inside them. Read-only: packs are authored in the repo, never through the API.
- platform:read
- Read across tenants as a PackAuth operator. The only scope in the system that is not bounded by a tenant, held only by operator keys, and every read it permits writes an audit row into the read tenant's own stream before the data is returned.
- products:read
- Read this tenant's products and their classification.
- products:write
- Create and amend products. Amending a classified product re-opens the checks that depended on the classification.
- releases:read
- Read print releases and verify one from its public reference.
- releases:write
- Cut a print release. Refused unless every blocking finding for the target markets is resolved or waived.
- runs:read
- Read compliance runs and their results.
- runs:write
- Start a compliance run against a manifest.
Things that are true of every operation
These were documented per-operation or nowhere, so an integrator learned each one by
hitting it. Every statement here is checked against the code by dev-crosscutting
— a promise about the API that the API stops keeping fails the build.
Authentication
Authorization: Bearer <key> on every operation except the 7
public ones. A key carries a fixed set of scopes; an operation refuses with 403
naming the scope it wanted. Keys are stored only as a hash — a lost key is revoked and
replaced, never recovered.
Errors
Every failure is {"error": {"code", "message", "detail"}, "request_id"}. The
code is stable and safe to branch on; the message is written for a
person and may change. Quote the request_id when asking about one.
409 rail_blocked is the one worth handling deliberately: a policy rail refused,
and failed_check names which. Its reason_code distinguishes a decision
from an outage — engine_unavailable means no verdict was reached and the action
was refused rather than allowed. PackAuth never fails open.
Idempotency
Send Idempotency-Key: <your id> on any write. A repeat with the same key
and the same body returns the first response instead of acting twice. A repeat with the same
key and a different body is refused — that is a bug in the caller, not a retry.
Without the header a retried write is a second write, and for a print release that means a
second certificate for one decision.
Pagination
Every list takes limit (default 50, max 500) and offset, and
answers {"data": [...], "has_more": bool}. has_more is computed by
fetching one row beyond the page, so it is exact rather than a guess from a full page.
Rate limits
There is no per-tenant rate limit today. Stating that plainly is more use
than a number nobody enforces: an integrator who plans around an invented limit has planned
around nothing. The one 429 in the API is the sandbox refusing to mint another
anonymous tenant once it is full. When a limit exists it will be documented here with its
headers, and this paragraph will say so.
What this page will not do
It fires reads. It will not fire a write while it points at production: a Send button next to a description of an operation that revokes an approval is one mis-click from changing a real tenant, and the copyable request above each write is the same call without that hazard. Writes become sendable when the sandbox surface exists and a mistake costs nothing.