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 responseLinkheader until there's no next link.
Quick start
- Make an initial request (optionally with filters and
per_page). - Read the response
Linkheader. - 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
Link headerList 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
cursor query paramThe 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
cursor with other paramsWhen you pass a cursor, it must be the only query parameter.
These will fail:
GET /v3/jobs?cursor=...&status=closedGET /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)
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)
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 linkRate 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-Remainingheader and slow down or pause when you approach the limit. - If you receive 429 Too Many Requests, read the
Retry-Afterheader 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
cursorwith any other query params. - Invalid/malformed
cursor. - Invalid
per_page(non-integer) or out of range.
- Passing
- 429 Too Many Requests
- You've hit rate limits while paginating. Read the
Retry-Afterheader and wait before retrying. See Rate Limiting.
- You've hit rate limits while paginating. Read the
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"}]
}Updated 16 days ago