Authentication
How tokens are minted, stored, presented and revoked.
Overview
The partner API uses long-lived org-scoped API tokens. Tokens are minted by organization admins in Settings → Integrations (or via the create_api_token RPC), and each token carries an explicit scope set chosen at mint time — a token can only reach the routes its scopes allow. Requests must include:
Authorization: Bearer stria_<36 hex characters>
Example:
Authorization: Bearer stria_a1b2c3d4e5f60718293a4b5c6d7e8f901234
Invalid format, unknown hash, or revoked token → 401.
Token format
| Property | Value |
|---|---|
| Prefix | stria_ (literal) |
| Secret | 36 lowercase hexadecimal characters ([a-f0-9]{36}) |
| Total length | 42 characters |
| Entropy | 18 random bytes |
The Edge Function validates the Authorization header with a strict regex before any database lookup.
Lifecycle
sequenceDiagram
participant Admin
participant App as Stria_Settings
participant DB as Postgres
participant API as Edge_Function
Admin->>App: Create token (name + scopes)
App->>DB: api_scope_catalogue()
DB-->>App: grantable scopes
App->>DB: create_api_token(org, name, scopes)
DB-->>App: plaintext once
App-->>Admin: Display + copy
Note over DB: Stores SHA-256 hash + prefix only
Admin->>API: Bearer stria_...
API->>DB: Lookup by hash
API->>DB: org_has_capability(api_access)
API->>DB: enforce_rate_limit(api:org)
API-->>Admin: 200/201 or error
Create
- Caller must be an org admin.
- Org must have
api_access. - Caller chooses scopes. Settings offers exactly what
api_scope_catalogue()returns, andcreate_api_tokenvalidates against the same function — an unknown scope is refused at mint time rather than stored and silently never matched. - Server generates the token, stores
token_hash = sha256(token)andtoken_prefix = left(token, 14), and writes the scope set explicitly. - Plaintext is returned once; it cannot be recovered later.
- Audit log records
token.createdwith name, prefix and scopes (never the secret).
Scopes
| Scope | Grants | Default in Settings |
|---|---|---|
client:read | Read clients | ✅ on |
project:read | Read projects and milestones | ✅ on |
time:read | Read time entries | ✅ on |
invoice:read | Read invoices and line items | ✅ on |
deliverable:read | Read deliverables and their review status | ✅ on |
time:write | Create time entries | off |
invoice:write | Create draft invoices | off |
deliverable:write | Create deliverables and add versions | off |
request:write | Ingest scope requests | off |
deliverable:send | Email a client a review link | off — sensitive |
invoice:issue | Issue invoices to clients | off — sensitive |
Read scopes are pre-ticked; every write scope starts off. Sensitive scopes are never pre-ticked, even when they are implemented.
Two scopes are separated from their obvious neighbour on purpose, both for the same reason — OWASP API6:2023, unrestricted access to sensitive business flows:
invoice:issueis separate frominvoice:writebecause issuing emails a client and starts a money flow, so it must never ride along on a scope granted for bookkeeping. No/v1route honours it yet; the scope exists so the capability can be added later without re-minting tokens.deliverable:sendis separate fromdeliverable:writebecause sending emails a client from the workspace's verified sender and starts a review clock. Filing an export and putting it in front of a client are different authorisations, and a design tool that only needs the first should not be handed the second.
A route whose scope the token lacks returns 403 with code: insufficient_scope and a required_scope field naming what is missing.
Omitting p_scopes
create_api_token(org, name) — the two-argument call shape — still works and yields ['deliverable:write'], which is what the original Figma plugin needed and nothing more.
That default now reaches exactly two /v1 routes — POST /v1/deliverables and POST /v1/deliverables/{id}/versions — and nothing else. It cannot read a deliverable back, cannot send one to a client, and reaches none of the client, project, time or invoice routes. If you are scripting against /v1, pass the scopes you need explicitly, or mint the token in Settings where they are selectable.
Before migration
20261113000000the Settings UI used that two-argument shape, so every token minted through the product held onlydeliverable:writeand returned403 insufficient_scopeon all eight/v1routes then available. If you hold a token minted before that change, its scopes are now visible in Settings → Integrations; re-mint it with the scopes you need.The Figma plugin needs
deliverable:read,deliverable:write,client:readandproject:read, plusdeliverable:sendif you want to send for approval from inside Figma. A token minted before20261224000012holds none of the read scopes, so the plugin will report which scope is missing and ask you to re-mint.
Revoke
- Admin calls revoke (Settings UI or
revoke_api_token). - Sets
revoked_at; subsequent API calls fail with 401. - Revoke does not require
api_access(supports cleanup after downgrade). - Audit log records
token.revoked.
Downgrade behavior
If the organization loses api_access while tokens still exist:
- Tokens remain in the database (admins can still list/revoke them).
- API requests return 403 with
code: upgrade_requiredandcapability: api_access. - New token minting is blocked.
Storage and transmission
| Rule | Detail |
|---|---|
| At rest | SHA-256 hex digest only; no plaintext column |
| In transit | HTTPS only (Supabase Edge + app) |
| In logs | Never log the full Bearer token; prefix is safe to display |
| In git / CI | Store secrets in a vault or encrypted CI variables — never commit tokens |
Least privilege recommendations
- Grant only the scopes the integration uses. A nightly reconciliation script needs
time:readandinvoice:read, not a write scope. - Create one token per integration (e.g. “Zapier”, “Figma plugin”) so revoke is surgical, and so one integration's scope set is not widened to suit another.
- Name tokens clearly; use the prefix shown in Settings to identify leaks. Scopes are listed beside each token, so an unexpected
403is diagnosable without re-minting. - Rotate by creating a new token, updating the consumer, then revoking the old token.
- Prefer environment variables or secret managers over hard-coded values in plugins.
Gateway headers
Supabase Function gateways usually require:
apikey: <SUPABASE_ANON_KEY>
Authorization: Bearer stria_...
The anon key identifies the project to the gateway. Authorization of the partner API is entirely the Stria token. Do not confuse the anon key with org credentials; it is public by design.
Related
- errors.md — 401 / 403 bodies
- rate-limits.md — per-org quotas
- endpoints.md — calling the API
More reference
- Endpoints — Every request and response shape, with worked examples.
- Outbound webhooks — Signed HTTPS POSTs when work is signed off, a change order moves, or an invoice is paid — plus how to verify one and how to survive a duplicate.
- Rate limits — Per organization, per minute, and published rather than discovered as a random failure.
- Errors — What each status means, what the body carries, and which ones to retry.
- Compatibility and versioning — What may change without notice, what may not, and how you are told.
- API changelog — Every change, dated, including the ones that were corrections.
OpenAPI specification · API access is on every paid plan · Start your 14-day trial