Skip to content
Docs
About By Dawn Contact supportOpen By Dawn
  1. Build reliably
  2. Pagination and rate limits

Pagination and rate limits

A busy project can hold thousands of tickets, so the API hands them over a page at a time, and it limits how fast any one key can ask. This page shows how to read every page, and how to stay inside the limits without having to think about them.

GET /tickets returns one page, plus a cursor that points to the next:

{ "ok": true, "data": { "items": [ … ], "nextCursor": "eyJyIjoiMDAwMDQyIn0", "total": 137 } }
  • limit sets the page size, from 1 to 200 (50 if you leave it out). Ask for more than 200 and you get 200.
  • nextCursor gets you the next page: send it back as cursor, with the same filters and sort, and keep going until it comes back null.
  • total counts every ticket that matches, not just this page. It can be missing, so don’t show “loaded so far” as if it were the total.

A cursor is a bookmark for one particular query. Pass it back untouched, and don’t use it with different filters, because it won’t mean anything there. The list tickets recipe pages through a whole project this way.

Everything else (comments, projects, columns, labels and cycles) comes back in one response, with no paging.

Do:

  • Filter in the request rather than in your code. GET /tickets takes statusId, cycleId, assigneeId, parentTicketId, open=true and a text search q, alongside projectId.
  • To find what changed since you last looked, ask for sort=updated&direction=desc and stop paging when you reach tickets you’ve already seen.

Don’t:

  • Fetch tickets one by one to check for changes. Each GET /tickets/{ticketId} counts against your rate limit, while one list call returns up to 200.
  • Decode, build or reuse a cursor across different filters.

The API counts requests in one-minute windows:

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

These numbers may change, so don’t build them into your code. Instead, react when you go over: the API answers 429 with { "ok": false, "error": "rate_limited" } and a Retry-After header saying how many seconds to wait. Wait that long, then send the same request again. The list tickets recipe does exactly that, in every language. Here it is in TypeScript:

list-tickets.ts (excerpt)
// A 429 means slow down: wait as long as Retry-After says, then try once more.
async function getWithRetry<T>(url: string): Promise<T> {
try {
return await call<T>(url, { token });
} catch (err) {
if (!(err instanceof ApiError && err.status === 429)) throw err;
await new Promise((resolve) => setTimeout(resolve, Math.min(err.retryAfter, 60) * 1000));
return call<T>(url, { token });
}
}

Most integrations never hit a limit if they:

  • keep one access token for its whole lifetime, instead of exchanging before each request;
  • ask for bigger pages with limit, rather than many small ones;
  • spread out the requests of several workers that share a key, since they all count against the same minute.