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

PropertyValue
Prefixstria_ (literal)
Secret36 lowercase hexadecimal characters ([a-f0-9]{36})
Total length42 characters
Entropy18 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

  1. Caller must be an org admin.
  2. Org must have api_access.
  3. Caller chooses scopes. Settings offers exactly what api_scope_catalogue() returns, and create_api_token validates against the same function — an unknown scope is refused at mint time rather than stored and silently never matched.
  4. Server generates the token, stores token_hash = sha256(token) and token_prefix = left(token, 14), and writes the scope set explicitly.
  5. Plaintext is returned once; it cannot be recovered later.
  6. Audit log records token.created with name, prefix and scopes (never the secret).

Scopes

ScopeGrantsDefault in Settings
client:readRead clients✅ on
project:readRead projects and milestones✅ on
time:readRead time entries✅ on
invoice:readRead invoices and line items✅ on
deliverable:readRead deliverables and their review status✅ on
time:writeCreate time entriesoff
invoice:writeCreate draft invoicesoff
deliverable:writeCreate deliverables and add versionsoff
request:writeIngest scope requestsoff
deliverable:sendEmail a client a review linkoff — sensitive
invoice:issueIssue invoices to clientsoff — 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:issue is separate from invoice:write because issuing emails a client and starts a money flow, so it must never ride along on a scope granted for bookkeeping. No /v1 route honours it yet; the scope exists so the capability can be added later without re-minting tokens.
  • deliverable:send is separate from deliverable:write because 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 20261113000000 the Settings UI used that two-argument shape, so every token minted through the product held only deliverable:write and returned 403 insufficient_scope on all eight /v1 routes 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:read and project:read, plus deliverable:send if you want to send for approval from inside Figma. A token minted before 20261224000012 holds none of the read scopes, so the plugin will report which scope is missing and ask you to re-mint.

Revoke

  1. Admin calls revoke (Settings UI or revoke_api_token).
  2. Sets revoked_at; subsequent API calls fail with 401.
  3. Revoke does not require api_access (supports cleanup after downgrade).
  4. 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_required and capability: api_access.
  • New token minting is blocked.

Storage and transmission

RuleDetail
At restSHA-256 hex digest only; no plaintext column
In transitHTTPS only (Supabase Edge + app)
In logsNever log the full Bearer token; prefix is safe to display
In git / CIStore 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:read and invoice: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 403 is 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