Skip to main content
Search pages by number, for result pages that people browse. Feeds page by cursor, for bulk export and sync.
Don’t page through search to export the corpus. Results shift as the data changes, so pages drop or repeat jobs, and every repeat is billed again. Use the feed for anything bulk.

GET /api/jobs · POST /api/jobs/search · GET /api/companies/{id}/jobs The response body carries total, page, page_size, and total_pages, mirrored in the X-Total-Count, X-Page, X-Page-Size, and X-Total-Pages response headers.
Search stops at a depth of 100,000 results (page × page_size). A deeper page is silently clamped to the last reachable page (page 1,000 at page_size 100): page and X-Page report the clamped page, but total_pages is not capped. Cap your page controls at that depth, and use the feed for anything deeper.

Stable pages over a fixed window

The default order is relevance, or most recently updated first when there is no text query. That order moves while you page: an updated job jumps to the front and shifts the rest by one, so a page-by-page pull can skip or repeat a job. To pull a window page by page without shifts:
  • Set sort to discovered_asc.
  • Bound the window with discovered_after and discovered_before. Both are inclusive and compare against created_at, the time Jobo first indexed the job, which never changes.
  • discovered_desc is also stable, but only with discovered_before set. Without it, newly indexed jobs push the later pages back.
A job that expires during the pull still leaves the results, so dedupe by id and compare the unique count with total.

Cursor-based pagination for feeds

POST /api/jobs/feed · POST /api/jobs/feed/managed · POST /api/jobs/feed/{feed_id} · GET /api/jobs/expired The response body carries next_cursor and has_more. Loop until has_more is false. The X-Has-More header mirrors it as True or False, so compare the header case-insensitively or read the body. GET /api/auto-apply/applications also pages by cursor, with limit (default 20) instead of batch_size.

Cursor rules

1

Send filters and batch_size only on the first request

The cursor embeds the first request’s filters, sort order, and batch_size. Filters and sort order sent with a cursor are ignored, so start a new scan to change them. A continuation can lower batch_size but never raise it. description_format is read on every request, continuations included.
2

Paginate serially

Keep one request in flight per cursor. Several continuations fired from the same cursor at once produce undefined results.
3

Persist next_cursor after every successful batch

A crashed run then resumes mid-stream instead of from the beginning. Feed cursors do not expire, so you can resume at any time. See Sync a database for a full checkpointing loop.
4

Leave stable_scan at its default of true

Pages follow immutable creation time, so jobs updated or expired mid-run cause no skips and no duplicates. stable_scan: false is the legacy update-recency order, and its cursors stop at 1,000,000 results.

Cursor failure modes

GET /api/jobs/expired never returns invalid_cursor. It ignores a cursor it cannot decode and returns the first page of the window again, so persist next_cursor exactly as returned. Errors has the full envelopes and retry guidance.

Example

First request, then a continuation whose body carries only cursor.

Python SDK

client.search.iter_jobs(...), client.feed.iter_jobs(...), client.feed.iter_managed_jobs(...), and client.feed.iter_expired_job_ids(...) page for you, by page number or cursor, and yield one item at a time. See Client libraries.
The feed iterators accept these filters as of 4.0.0: On 3.x and earlier, iter_jobs on client.feed takes only locations, sources, work_models, posted_after, and batch_size, so incremental sync through the client needs 4.0.0. For any filter the clients don’t send, call the endpoint over plain HTTPS as in the example.