Gallery Platform API

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.

PrincipalHow the gallery/org is chosen
gpk_ gallery keyImplicit in the key.
opk_ org keyImplicit 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.

ScopeCovers
contacts.pii:readA contact's private notes.
artworks.financial:readA 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.

On this page