---
updatedAt: 2026-01-29T18:53:00.000Z
agentTools:
  projectIndex: https://harvestdocs.greenhouse.io/llms.txt
---

# Pagination

Harvest v3 uses **cursor-based pagination** for list endpoints.

The rule of thumb:

* Make your first request with whatever filters you need.
* Then, keep calling the `rel="next"` URL from the response `Link` header until there's no next link.

## Quick start

1. Make an initial request (optionally with filters and `per_page`).
2. Read the response `Link` header.
3. If the header contains a `rel="next"` link, request that URL next.

Example first request:

```bash
curl --location 'https://harvest.greenhouse.io/v3/jobs?status=closed&per_page=2' \
  --header 'Authorization: Bearer <<ACCESS_TOKEN>>'
```

Example `Link` header returned (format):

```http
Link: <https://harvest.greenhouse.io/v3/jobs?cursor=received_cursor_string...>; rel="next"
```

Example next request (use the URL from the header):

```bash
curl --location 'https://harvest.greenhouse.io/v3/jobs?cursor=received_cursor_string...' \
  --header 'Authorization: Bearer <<ACCESS_TOKEN>>'
```

When the `Link` header is missing or empty, you're on the last page.

## How it works

### The `Link` header

List responses include a W3C Link header (RFC 5988) with a `rel="next"` link when there is another page:

```http
Link: <https://harvest.greenhouse.io/v3/jobs?cursor=received_cursor_string...>; rel="next"
```

Harvest v3 currently returns only a `next` link (no `prev` or `last`).

### The `cursor` query param

The `cursor` value is a URL-safe, Base64-encoded payload that contains the information needed to paginate through the records you initially requested.

Treat the cursor as an **opaque** value: don't parse it, and don't try to construct it yourself. Always take it from the `Link` header. Its size may grow or shrink based on the needs of the system and the filters provided.

### Don't combine `cursor` with other params

When you pass a `cursor`, **it must be the only query parameter**.

These will fail:

* `GET /v3/jobs?cursor=...&status=closed`
* `GET /v3/jobs?cursor=...&per_page=50`

Instead, put filters and `per_page` on the **first** request only, then follow the `Link` header exactly.

### Page size (`per_page`)

* **Default**: `100`
* **Minimum**: `1`
* **Maximum**: `500`

### Ordering

Harvest v3 paginates by primary key (`id`) in descending order. The cursor advances by `id` to continue where the previous page left off.

## Parsing `Link` headers (pseudo-code)

```text
next_url = initial_url

while next_url is present:
  response = GET(next_url)
  process(response.body)

  link = response.headers["Link"]
  next_url = parse_next_url_from_link_header(link) # returns null when no next link
```

## Rate limiting and pagination

Each paginated request counts against your rate limit. When iterating through many pages:

* Use `per_page` (up to 500) to reduce the number of requests for large datasets.
* Monitor the `X-RateLimit-Remaining` header and slow down or pause when you approach the limit.
* If you receive **429 Too Many Requests**, read the `Retry-After` header and wait before retrying the same request.

See [Rate Limiting](https://harvestdocs.greenhouse.io/docs/api-rate-limiting) for full details on headers, limits, and handling 429 errors.

## Common errors

* **422 Unprocessable Content**
  * Passing `cursor` with any other query params.
  * Invalid/malformed `cursor`.
  * Invalid `per_page` (non-integer) or out of range.
* **429 Too Many Requests**
  * You've hit rate limits while paginating. Read the `Retry-After` header and wait before retrying. See [Rate Limiting](https://harvestdocs.greenhouse.io/docs/api-rate-limiting).

Example error (cursor combined with other query params):

```json
{
  "message": "Unprocessable Content",
  "errors": ["When passing a cursor, do not include other query params."]
}
```

Example error (`per_page` out of range):

```json
{
  "message": "Unprocessable Content",
  "errors": [{"per_page": "`600` number is greater than: 500"}]
}
```