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, contacts, offers, members, invitations, sales,
payments, payables, deals, locations, reports, publications,
inbox. Note there is no separate invoices scope — invoices are part of the
sales resource.
Org plane — the same pairs for: org, members, invitations,
galleries, reports, billing.
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.