# Pagination and rate limits

> Read long lists a page at a time, and stay comfortably inside the API's 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.

## Pagination

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

```json
{ "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](https://docs.bydawn.com/developers/recipes/list-tickets/) 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.

## Rate limits

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](https://docs.bydawn.com/developers/recipes/list-tickets/) does exactly that, in every language. Here it is in
TypeScript:

list-tickets.ts (excerpt):

```ts
// 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.

Source: https://docs.bydawn.com/developers/pagination-and-rate-limits/
