---
updatedAt: 2026-08-27T14:00:37.000Z
agentTools:
  projectIndex: https://developers.ashbyhq.com/llms.txt
---

# Pagination and Incremental Synchronization

Many of the `list` endpoints in Ashby's API (for example, `candidate.list` and `job.list`) implement pagination and incremental synchronization. This guide describes how to:

1. Implement the client-side logic needed to iterate through multiple pages of results, and
2. Only fetch resources that have been updated since the previous API request

**Note:** Not all paginated endpoints support incremental sync. You can tell whether an endpoint supports incremental sync by checking if the `syncToken` parameter is available in its request schema. Endpoints without `syncToken` only support pagination (full sync).

# Full sync

Paginated APIs default to a "full" sync, returning data for *all* the resources of the requested object type. Full syncs can be slow. Ideally, clients should only need to perform one initial full sync, and then switch to more-efficient incremental syncs (see below).

To begin a full sync, `POST` an empty JSON object to the target endpoint. If the request succeeds, it will return an object like:

```json5
{
  success: true,
  results: [
    // An array of the requested object type
  ],
  moreDataAvailable: true,
  nextCursor: "some-token-value",
}
```

The length of the `results` array will be automatically capped by the API. If you'd like to specify the number of results returned per page, you can do so by specifying a `limit` in your API request:

```json
{
  "limit": 100
}
```

If the `moreDataAvailable` field in the response is `true`, then there are additional pages of objects to fetch. To fetch the next page, re-send a request to the endpoint, this time passing the value of `nextCursor` from the latest response as a parameter:

```json
{
  "limit": 100,
  "cursor": "some-token-value"
}
```

Repeat this cycle on the client side, replacing `cursor` with the latest `nextCursor` on each request, until you receive a response with `moreDataAvailable` set to `false`. This signals that you've fetched all available records of the targeted type, completing the full sync. In this case, the response will contain a new `syncToken` field:

```json5
{
  success: true,
  results: [
    // Array of requested object type
  ],
  moreDataAvailable: false,
  syncToken: "sync-token-value",
}
```

You should store this value of `syncToken` for use in your next sync.

## Example

To list all the candidates in my organization, I would begin with the request:

```shell
curl -XPOST https://api.ashbyhq.com/candidate.list -u "${API_KEY}:" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{ "limit": 25 }'
```

Returning:

```json5
{
  success: true,
  results: [
    // 25 objects
  ],
  moreDataAvailable: true,
  nextCursor: "Rl",
}
```

I'd then send a follow-up request:

```shell
curl -XPOST https://api.ashbyhq.com/candidate.list -u "${API_KEY}:" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{ "limit": 25, "cursor": "Rl" }'
```

Returning:

```json5
{
  success: true,
  results: [
    // <= 25 objects
  ],
  moreDataAvailable: false,
  syncToken: "mqXvvQBWO",
}
```

I would then persist the `syncToken` value of `"mqXvvQBWO"` for later use.

# Incremental sync

Incremental sync allows you to fetch *only* the resources that have been modified since your last sync request. To do this, include the `syncToken` returned at the end of your last sync flow in your initial API request:

```json
{
  "syncToken": "sync-token-value"
}
```

The API will return a response like:

```json5
{
  success: true,
  results: [
    // An array of the requested object type
  ],
  moreDataAvailable: true,
  nextCursor: "some-token-value",
  syncToken: "sync-token-value",
}
```

Note here that as long as `moreDataAvailable` is `true`, the `syncToken` returned by the API will be the same as the `syncToken` passed in the request.

To fetch the next page of results, pass the latest values of `nextCursor` *and* `syncToken` in your request:

```json
{
  "cursor": "some-token-value",
  "syncToken": "sync-token-value"
}
```

Repeat this flow, passing a *new* `cursor` and the *same* `syncToken` in each request, until you receive a response with `moreDataAvailable` set to `false`. This response will contain a new `syncToken` value for you to persist for the next set of requests:

```json5
{
  success: true,
  results: [
    // Array of requested object type
  ],
  moreDataAvailable: false,
  syncToken: "new-sync-token-value",
}
```

## Example

To list all the candidates in my organization that have been updated since my last sync, I would take the `syncToken` value from that sync and pass it in a request:

```shell
curl -XPOST https://api.ashbyhq.com/candidate.list -u "${API_KEY}:" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{ "limit": 25, "syncToken": "mqXvvQBWO" }'
```

Returning:

```json5
{
  success: true,
  results: [
    // 25 objects
  ],
  moreDataAvailable: true,
  nextCursor: "Rl",
  syncToken: "mqXvvQBWO",
}
```

I'd then send a follow-up request:

curl -XPOST <https://api.ashbyhq.com/candidate.list> -u "${API_KEY}:" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{ "limit": 25, "syncToken": "mqXvvQBWO", "cursor": "Rl" }'

Returning:

```json5
{
  success: true,
  results: [
    // <= 25 objects
  ],
  moreDataAvailable: false,
  syncToken: "PjJ33rQZl",
}
```

I would then persist the new `syncToken` value of `"PjJ33rQZl"`, replacing the previous value of `"mqXvvQBWO"`, for later use.

# Incremental sync page depth limit

Incremental syncs are limited to a maximum number of pages. If your incremental sync exceeds this limit, the API will return an error:

```json
{
  "success": false,
  "errors": [
    {
      "code": "incremental_sync_too_large",
      "message": "Too many pages for incremental sync. Please perform a full sync instead by omitting the syncToken parameter."
    }
  ]
}
```

This typically occurs when a very large number of records have been updated since your last sync (e.g. due to a bulk data migration). The current limit is 100 pages (10,000 results at the default page size). If your client receives this error, it should discard the current `syncToken` and begin a new full sync to re-fetch all data.

To minimize the chance of hitting this limit, we recommend performing incremental syncs frequently (at least once per day).

# Cursor expiration

A `nextCursor` is only valid for 14 days from the time the sync it belongs to started. If you pass a cursor older than that, the API will return:

```json
{
  "success": false,
  "errors": ["next_cursor_expired"],
  "errorInfo": {
    "code": "next_cursor_expired",
    "message": "The cursor has expired. Cursors are valid for 14 days. Please discard it and start a new sync by omitting the cursor parameter."
  }
}
```

An invalid cursor returns one of two error codes:

* `cursor_invalid` when the cursor string itself is malformed or cannot be used by the endpoint.
* `invalid_next_cursor` when the cursor can be decoded but has an invalid structure or is incompatible with the request, such as when it was created for a different pagination mode.

Recover from either error the same way: discard the cursor and start the sync again from the first page. Always pass back the exact `nextCursor` string from the previous response — cursors are opaque, and a modified cursor may be silently interpreted as a different position rather than rejected.

# Sync token expiration

Sync tokens returned by the API can be invalidated for various reasons, including token expiration. Sync tokens are valid for 14 days. When the API is passed an expired token, it will return a response like:

```json
{
  "success": false,
  "errors": ["sync_token_expired"],
  "errorInfo": {
    "code": "sync_token_expired",
    "message": "The syncToken has expired. Sync tokens are valid for 14 days. Please perform a full sync by omitting both the syncToken and cursor parameters."
  }
}
```

If the token cannot be decoded at all, the API returns `sync_token_invalid` instead. In either case, your client should begin a new full sync to re-fetch all data using valid tokens. Drop the `cursor` as well as the `syncToken` — `cursor` controls your position in the result set independently of `syncToken`, so retrying with a retained cursor resumes mid-sync and skips every record before it.

To minimize the risk of sync token expiration, we recommend performing an incremental sync at least once per week.