Authentication
How API keys, scopes, and the two request principals work — and how the gallery is chosen for each request.
Every /api/v1 request runs as one of two principals against the same
handlers. The database's row-level security (RLS) is the real access boundary in
both cases — an application-layer bug can't cross tenants.
Principals
API key (gpk_ / opk_)
An external key acts as a gallery (or organization), not a person. The gateway verifies the key, then mints a short-lived (~60s) scoped token whose claims carry the key's gallery/org id and its granted scopes. Queries then run under claim-based RLS. This is the principal you use for integrations.
- A
gpk_gallery key — the gallery is implicit in the key. Endpoints are under/api/v1/gallery/*(singular; there is no slug in the path). - An
opk_org key — the organization is implicit. Endpoints are under/api/v1/org/*.
User token (first-party)
The logged-in app forwards the signed-in user's session token; the gateway re-verifies it and runs under the user's existing membership RLS. Because a user can belong to many galleries, the gallery is named per request with a header (see below). You generally won't use this principal directly — it's how the app itself dogfoods the API.
Sending credentials
Authorization: Bearer gpk_…The x-api-key: gpk_… header is accepted as an alternative. A missing or
unrecognized credential returns 401; keys are matched by a non-reversible hash
(HMAC-SHA256 with a server-side pepper), so the raw key is never stored.
Choosing the gallery / org
| Principal | How the gallery/org is chosen |
|---|---|
gpk_ gallery key | Implicit in the key. |
opk_ org key | Implicit in the key. |
| User token (gallery plane) | X-Gallery-Id or X-Gallery-Slug header. |
| User token (org plane) | X-Org-Id or X-Org-Slug header. |
The target is never taken from the request body — always from the key or a
header. Naming a gallery you can't see returns 404 (never 403), so the API
doesn't leak the existence of other galleries' resources.
Scopes
Keys carry resource:action scopes. A write scope implies the matching read
scope, so a key never needs both listed — grant artworks:write and reads are
included.
Gallery plane — a resource:read / resource:write pair for each of:
gallery, artworks, artists, contacts, offers, campaigns, drops, members, invitations, sales,
payments, payables, deals, locations, reports, publications,
inbox, events, shows, tags, jobs, traffic, legal_documents,
contacts.pii, artworks.financial. Note there is no separate invoices scope —
invoices are part of the sales resource, and shows covers shows, art fairs,
and programs. There is no programs scope — programs ride the shows scope, and
the events scope is the platform event log, not the calendar. The tags
scope governs the tag vocabulary (the catalog); applying a tag to a record
rides that record's own scope (tagging an invoice, for example, needs sales:write).
Org plane — the same pairs for: org, members, invitations,
galleries, reports, billing.
The two sensitive-read tiers
contacts.pii and artworks.financial are separate resources on purpose, and
a write scope on the parent does not imply them.
| Scope | Covers |
|---|---|
contacts.pii:read | A contact's private notes. |
artworks.financial:read | A work's cost basis, expenses, appraisals, insured value and price history. |
Grant them only to a key that needs them. An endpoint whose whole answer is
tiered data refuses a key without the tier with a 403 naming the scope, rather
than returning an empty list. Where tiered data is one field inside a larger
record — a contact's private_notes — the field is omitted and the rest of the
record comes back.
Some endpoints accept the tier or a related scope, because the same rows
answer more than one question: a work's expenses, for example, are readable with
artworks.financial:read, sales:read or payables:read. The operation's page
in the API reference lists every accepted combination.
When you create a key you pick per-resource access (None / Read / Read &
write); that choice expands into the scope list at mint time. Each operation's
required scope is shown on its page in the
API reference and in the openapi.json security
block.
A request that lacks the scope for an operation returns 403 with code
forbidden. The in-code scope check is a fast 403; the database policy is the
real ceiling, so a scope can only ever narrow what a principal could otherwise
do — never widen it.
Key lifecycle & expiry
Keys may carry an expiry; an expired key returns 403. Rotate and revoke keys
where they were created — the gallery's Developer → API keys section, or the
organization's Settings page for org keys. Revocation takes effect
immediately because every request re-checks the key against the store.
A key can also be disabled rather than revoked. Disabling is the reversible
stop: the key keeps its scopes and its usage history, and requests are refused
with 403 and detail This API key is disabled. until it is switched back on
from the same panel. Revoking is permanent. If you are integrating and your
calls start returning that error, the gallery owner has switched your key off —
ask them to re-enable it rather than minting a new one.