Authentication
The SAW API uses two credentials:
- An API key (
saw_live_…) identifies your integration. Organization admins create keys in Settings → Security → API keys. A key is long-lived, so keep it server-side and never embed one in a browser page or a mobile app. - An access token authorizes requests. It is a short-lived JWT, valid for about 15 minutes. You get one by exchanging the key.
Exchange a key for an access token
Section titled “Exchange a key for an access token”The exchange is served by the Accounts API host:
POST https://accounts.dev.sawrun.com/api/v1/auth/token/api-keyContent-Type: application/json
{ "apiKey": "saw_live_…" }{ "ok": true, "data": { "accessToken": "eyJ…", "tokenType": "Bearer", "expiresInSeconds": 900 } }Send the token on every request to the workspace host:
GET https://workspace.dev.sawrun.com/api/v1/orgs/{orgId}/projectsAuthorization: Bearer eyJ…Reuse a token until shortly before expiresInSeconds runs out, then exchange again. Don’t
exchange on every request, because the exchange has its own
rate limit. If a call answers 401 unauthorized, the
token has expired or its key was revoked: exchange once more, and stop if that fails. This is
how saw.ts does it:
let token: { value: string; orgId: string; expiresAt: number } | undefined;
async function accessToken() { // Access tokens live about 15 minutes; exchange again a minute before expiry. if (!token || Date.now() > token.expiresAt - 60_000) { const exchange: Schemas['ApiKeyExchange'] = { apiKey }; const data = await send<Schemas['AccessTokenData']['data']>(`${ACCOUNTS_URL}/auth/token/api-key`, 'post', exchange); token = { value: data.accessToken, orgId: orgId ?? orgIdOf(data.accessToken), expiresAt: Date.now() + data.expiresInSeconds * 1000, }; } return token;}Your organization ID
Section titled “Your organization ID”Every workspace path starts with /orgs/{orgId}. A key belongs to exactly one organization,
and the access token names it in its org_id claim:
/** The organization an access token acts for: its `org_id` claim. */export function orgIdOf(accessToken: string): string { const payload = accessToken.split('.')[1] ?? ''; const claims = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))) as { org_id?: string }; if (!claims.org_id) throw new Error('The access token names no organization.'); return claims.org_id;}A request for any other organization is refused with 403 forbidden.
Scopes
Section titled “Scopes”A key’s Access setting decides what its tokens can do:
| Access in Settings | Scopes | Allows |
|---|---|---|
| Board — read only | tickets:read |
Read tickets, comments, projects, statuses, labels and cycles |
| Board — read & write | tickets:read, tickets:write |
Also create, update, move and archive tickets; post comments; manage relations |
An operation the key’s scopes don’t cover answers 403 forbidden. A key also can’t:
- list tickets across the whole organization.
GET /ticketsneeds aprojectId. - delete a ticket. Archive it instead (
POST /tickets/{ticketId}/archive). - create labels. That needs a signed-in organization admin.
- move a ticket into a column with the
buildingrole. That would start an agent run. - see restricted projects. They don’t appear in its lists, and their tickets are refused.
Tickets and comments a key creates are attributed to the key, not to a person.
Revoking and expiring keys
Section titled “Revoking and expiring keys”Revoke a key in Settings → Security. A revoked or expired key can no longer be exchanged, and access tokens already issued from it stop working within seconds. Choose an expiry when you create the key (30, 90 or 365 days, or never), and rotate keys by creating the new one before revoking the old one.