Pagination and rate limits
Pagination
Section titled “Pagination”GET /tickets returns one page at a time:
{ "ok": true, "data": { "items": [ … ], "nextCursor": "eyJyIjoiMDAwMDQyIn0", "total": 137 } }limitsets the page size: 1 to 200, default 50. A larger value is treated as 200.nextCursorfetches the next page. Pass it back ascursor, with the same filters and sort, untilnextCursorisnull.totalcounts 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.
Rate limits
Section titled “Rate limits”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:
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
limitto 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.