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:

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

Example Link header returned (format):

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

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

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:

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)

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 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.

Example error (cursor combined with other query params):

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

Example error (per_page out of range):

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