Responses and errors
Every response, success or failure, uses one JSON envelope.
On success, ok is true and the payload is in data:
{ "ok": true, "data": { "id": "ticket-…", "identifier": "SAW-42", "title": "Export the board as CSV" } }On failure, ok is false and error is a stable, machine-readable slug. message, when
present, is human-readable detail for logs. Never parse it:
{ "ok": false, "error": "ticket_not_found" }Branch on the error slug, not on the HTTP status text. Slugs are part of the API contract, and
changes to them are recorded in the API changelog.
The slugs you will meet most
Section titled “The slugs you will meet most”| Status | error |
Meaning | What to do |
|---|---|---|---|
| 401 | invalid_api_key |
The key exchange was refused: the key is wrong, revoked or expired. | Check the key, or create a new one. |
| 401 | unauthorized |
No access token, or it has expired. | Exchange the key again, once. |
| 403 | forbidden |
The token doesn’t grant this operation in this organization. | Check the key’s scopes. |
| 404 | not_found, ticket_not_found, project_not_found |
The resource doesn’t exist in this organization. | Don’t retry. |
| 422 | validation_error, invalid_status, invalid_project, … |
The body or query is invalid. message says which field. |
Fix the request. Don’t retry. |
| 429 | rate_limited |
Too many requests. | Wait Retry-After seconds, then retry. See rate limits. |
The API reference lists every error response of each operation, and the full
slug vocabulary is in its ApiErrorSlug and AccountsApiErrorSlug schemas. Treat a slug you
don’t recognize like its HTTP status class.
Recipe: handle an error by its slug
Section titled “Recipe: handle an error by its slug”saw.ts turns a failure into a SawApiError whose error is the slug. This recipe triggers
three real failures (a missing project, an invalid ticket and a wrong API key) and handles each one by its slug:
// Handle errors by their slug. Every failure is `{ "ok": false, "error": "<slug>" }`;// saw.ts turns it into a SawApiError whose `error` is that slug.//// node errors.tsimport { createClient, SawApiError } from './saw.ts';
const saw = createClient();
// Run a call that should fail, and return the SawApiError it failed with.async function failure(call: () => Promise<unknown>): Promise<SawApiError> { try { await call(); } catch (err) { if (err instanceof SawApiError) return err; throw err; } throw new Error('Expected the call to fail.');}
// A resource that does not exist: 404.const missing = await failure(() => saw.get('/projects/{projectId}', { path: { projectId: 'project-00000000-0000-0000-0000-000000000000' } }),);switch (missing.error) { case 'project_not_found': case 'not_found': console.log(`${missing.status} ${missing.error}: no such project in this organization.`); break; default: throw missing;}
// An invalid request: 422. The message says what is wrong; it is for people, so log it, don't parse it.const [project] = await saw.get('/projects');if (!project) throw new Error('This organization has no projects yet.');const invalid = await failure(() => saw.post('/tickets', { body: { projectId: project.id, title: '' } }));if (invalid.error !== 'validation_error') throw invalid;console.log(invalid.message);
// An API key that is wrong or revoked: the token exchange is refused.const refused = await failure(() => createClient('saw_live_not_a_real_key').get('/projects'));if (refused.error !== 'invalid_api_key') throw refused;console.log(`${refused.status} ${refused.error}: check the key, or create a new one in Settings → Security.`);SAW_API_KEY=saw_live_… node --experimental-strip-types errors.ts