Skip to content
The SAW API is in beta.

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.

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.

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:

errors.ts
// 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.ts
import { 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.`);
Terminal window
SAW_API_KEY=saw_live_… node --experimental-strip-types errors.ts