> ## Documentation Index
> Fetch the complete documentation index at: https://jobo.world/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> The two pagination models the API uses, when to use each, and the rules that keep cursors reliable.

<Warning>
  The API has **two pagination models and they are not interchangeable**. Search endpoints page by number; feed endpoints page by cursor. Using the wrong one for your use case wastes credits and, for bulk export, drops or duplicates records as data changes underneath you.
</Warning>

***

## Page-based — search

`GET /api/jobs` · `POST /api/jobs/search` · `GET /api/companies/{id}/jobs`

**Request**

| Parameter   | Default | Notes                                                                        |
| ----------- | ------- | ---------------------------------------------------------------------------- |
| `page`      | `1`     | 1-indexed.                                                                   |
| `page_size` | `25`    | Valid range `1`–`100`. Out of range is a real `400` — see [Errors](/docs/errors). |

**Response**

`total`, `page`, `page_size`, `total_pages` in the body. Mirrored in `X-Total-Count`, `X-Page`, `X-Page-Size`, `X-Total-Pages` — see [Response headers](/docs/response-headers).

Use page-based pagination for UI result pages and anything a human is looking at. **Do not use it to bulk-export the corpus — use the feed.** The feed is cheaper per job and immune to records shifting between pages as data changes; deep page-based pagination over a moving dataset can skip or repeat records.

## Cursor-based — feed

`POST /api/jobs/feed` · `POST /api/jobs/feed/managed` · `GET /api/jobs/expired`

**Request**

| Parameter              | Default                   | Max     |
| ---------------------- | ------------------------- | ------- |
| `cursor`               | — (omit on first request) | —       |
| `batch_size` (feed)    | `1000`                    | `1000`  |
| `batch_size` (expired) | `1000`                    | `10000` |

**Response**

`next_cursor`, `has_more` in the body, mirrored by `X-Has-More`. Loop until `has_more` is `false`.

`GET /api/auto-apply/applications` also uses a cursor, with `limit` (default `20`) in place of `batch_size`.

### The four rules that matter

<Steps>
  <Step title="Send filters and batch_size only on the first request">
    The cursor embeds them. A continuation body carries *only* the cursor — any other values you send alongside it are silently ignored. This is the most common surprise: changing a filter mid-pagination does nothing.
  </Step>

  <Step title="Paginate serially">
    One in-flight request per cursor. Cursors are not designed for parallel fan-out — firing several continuations from the same cursor concurrently produces undefined results.
  </Step>

  <Step title="Persist next_cursor after every successful batch">
    So a crash resumes mid-stream instead of restarting from the beginning. See [Sync a database](/docs/guides/sync-a-database) for a full checkpointing loop.
  </Step>

  <Step title="Leave stable_scan at its default of true">
    Pages follow immutable creation time, so records cannot shift across page boundaries while you paginate — no skips, no duplicates, even if jobs are updated or expire mid-run.
  </Step>
</Steps>

### Cursor failure modes

| Status | `code`                         | Response                                                                                                               |
| ------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_cursor`               | The cursor could not be decoded. Discard it and restart pagination from the beginning — never loop on the same value.  |
| `409`  | `feed_cursor_restart_required` | A legacy non-stable cursor hit the deep-pagination boundary. Restart with `{"stable_scan": true, "batch_size": 1000}`. |

Full envelopes and retry guidance are on [Errors](/docs/errors).

***

## Example

First request, then a continuation. Note the continuation body carries only `cursor`.

<CodeGroup>
  ```bash first-request.sh theme={null}
  curl -X POST "https://connect.jobo.world/api/jobs/feed" \
    -H "X-Api-Key: $JOBO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"sources": ["greenhouse", "lever"], "batch_size": 1000}'
  ```

  ```bash continuation.sh theme={null}
  curl -X POST "https://connect.jobo.world/api/jobs/feed" \
    -H "X-Api-Key: $JOBO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"cursor": "eyJvZmZzZXQiOjEwMDB9"}'
  ```

  ```python paginate.py theme={null}
  import httpx

  BASE = "https://connect.jobo.world"
  headers = {"X-Api-Key": "YOUR_API_KEY"}

  body = {"sources": ["greenhouse", "lever"], "batch_size": 1000}
  cursor = None

  with httpx.Client(base_url=BASE, headers=headers) as client:
      while True:
          payload = {"cursor": cursor} if cursor else body
          response = client.post("/api/jobs/feed", json=payload)
          response.raise_for_status()
          data = response.json()

          for job in data["jobs"]:
              ...  # upsert by job["id"]

          if not data["has_more"]:
              break
          cursor = data["next_cursor"]  # persist this before the next request
  ```
</CodeGroup>

## Python SDK

`client.search.iter_jobs(...)`, `client.feed.iter_jobs(...)`, and `client.feed.iter_expired_job_ids(...)` handle pagination for you — page-based and cursor-based alike — yielding one item at a time.

```python theme={null}
from jobo_enterprise import JoboClient

with JoboClient(api_key="YOUR_API_KEY") as client:
    for job in client.feed.iter_jobs(sources=["greenhouse", "lever"], batch_size=1000):
        ...  # upsert by job.id

    for job_id in client.feed.iter_expired_job_ids(expired_since="2026-07-01T00:00:00Z"):
        ...  # mark expired
```

`iter_jobs` on `client.feed` takes `locations`, `sources`, `work_models`, `posted_after`, and `batch_size` — it does not expose `employment_types`, `experience_levels`, `updated_after`, or `stable_scan`. Use the raw endpoints in [Sync a database](/docs/guides/sync-a-database) if you need those.

***

See [Sync a database](/docs/guides/sync-a-database) for the complete incremental-sync loop with checkpointing, and [Errors](/docs/errors) for full error envelopes.
