Skip to content
The SAW API is in beta.

Pagination and rate limits

GET /tickets returns one page at a time:

{ "ok": true, "data": { "items": [ … ], "nextCursor": "eyJyIjoiMDAwMDQyIn0", "total": 137 } }
  • limit sets the page size: 1 to 200, default 50. A larger value is treated as 200.
  • nextCursor fetches the next page. Pass it back as cursor, with the same filters and sort, until nextCursor is null.
  • total counts every ticket matching the query, not just this page. It is optional: if it is absent, don’t present the number of tickets loaded so far as a total.

Treat a cursor as opaque: don’t decode or build one, and don’t reuse it with different filters. The list tickets recipe pages through a whole project.

Comments, projects, statuses, labels and cycles return their whole list in one response.

Requests are counted in fixed one-minute windows:

Host Counted per Default limit
Workspace (workspace.dev.sawrun.com) API key 300 requests per minute
Key exchange (accounts.dev.sawrun.com/…/auth/token/api-key) Client IP address 30 requests per minute

The limits may change, so don’t hard-code them. Over a limit, the API answers 429 with { "ok": false, "error": "rate_limited" } and a Retry-After header giving the seconds to wait. Wait that long, then retry. saw.ts does this once per request:

saw.ts (excerpt)
async function send<T>(url: string, method: string, body?: unknown, token?: string): Promise<T> {
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, {
method: method.toUpperCase(),
headers: {
Accept: 'application/json',
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
// Rate limited: wait as long as the server asks, then retry once.
if (res.status === 429 && attempt === 1) {
const seconds = Number(res.headers.get('Retry-After')) || 1;
await new Promise((resolve) => setTimeout(resolve, Math.min(seconds, 60) * 1000));
continue;
}
const envelope = (await res.json().catch(() => null)) as Envelope<T> | null;
if (envelope?.ok === true) return envelope.data;
throw new SawApiError(res.status, envelope?.error ?? 'internal_error', envelope?.message);
}
}

To stay under the limits:

  • Reuse an access token for its whole lifetime instead of exchanging per request.
  • Use limit to fetch larger pages rather than many small ones.
  • For a key shared by several workers, spread their requests out, because they all count against one window.