Get started / Pagination

Pagination

Cursor and offset paging, their limits, and how to walk a large result set without missing rows.

Every list endpoint pages with a cursor: the response carries a next_cursor, and you pass it back as ?cursor= to get the next page. offset is still there for shallow, random-access paging. There are no Link headers.

BASH
curl -H "Authorization: Bearer $HYPERJOBS_KEY" \
  "https://api.hyperjobs.io/v1/jobs?skill=python&limit=200"
JSON
{
  "data": [  ],
  "total": 21497,
  "limit": 200,
  "offset": 0,
  "next_cursor": "eyJwIjoiMjAyNi0wNy0wOCAxNDowMjoxMSIsImlkIjoiYXNoYnk6N2MxZjJhOTQuLi4ifQ"
}

total is the count of all matching rows, not the page. The page itself is data.length, which is limit until the last page.

Cursors#

next_cursor is an opaque base64url keyset cursor — don't parse it, just send it back:

BASH
curl -H "Authorization: Bearer $HYPERJOBS_KEY" \
  "https://api.hyperjobs.io/v1/jobs?skill=python&limit=200&cursor=eyJwIjoiMjAyNi0wNy0wOCAx…"
  • null means done. The last page carries "next_cursor": null.
  • Stable at any depth. A cursor pins your position by key, not by row count, so page 500 costs the same as page 2 and rows ingested mid-walk can't shift under you. See deep paging below.
  • Cursor wins over offset. On /v1/jobs, passing cursor ignores offset, and the response omits the offset key. Pick one style per walk.
  • A malformed or hand-edited cursor is a 400, not an empty page.
  • The feeds — /v1/jobs/feed, /v1/jobs/expired, /v1/jobs/modified — return rows in ascending index order and page the same way: follow next_cursor until null and you've drained the window.

A collapse response has no next_cursor — collapsed groups have no stable keyset to cursor over. Page those with offset, which collapse supports as usual.

Offset#

/v1/jobs and /v1/companies also take offset (min 0, max 100,000) for shallow or random-access paging — jumping straight to page 12 of a UI, say. Beyond 100,000, or for any full walk, use the cursor.

Limits#

EndpointDefault limitMax limitCursor?Offset?
/v1/jobs50200YesYes (max 100,000)
/v1/jobs/feed2001,000YesNo
/v1/jobs/expired1,0005,000YesNo
/v1/jobs/modified2001,000YesNo
/v1/companies50200NoYes (max 100,000)

Out-of-range values are rejected, not clamped: limit=5000 on /v1/jobs is a 400 with "hint": "maximum is 200", and the minimum everywhere is 1 — limit=0 and limit=-1 are 400s too. (An early draft of this API had a negative-limit-returns-everything backdoor; it's gone.)

Walking a full result set#

JS
async function* allJobs(params, key) {
  const PAGE = 200; // the max — fewer, bigger pages cost less quota
  let cursor = null;
 
  for (;;) {
    const qs = new URLSearchParams({ ...params, limit: PAGE });
    if (cursor) qs.set("cursor", cursor);
    const res = await fetch(`https://api.hyperjobs.io/v1/jobs?${qs}`, {
      headers: { Authorization: `Bearer ${key}` },
    });
    if (!res.ok) throw new Error(`${res.status}`);
 
    const { data, next_cursor } = await res.json();
    yield* data;
 
    if (!next_cursor) return;
    cursor = next_cursor;
  }
}

No total bookkeeping, no short-page heuristics — the cursor is the loop condition.

Deep paging drifts#

Results are sorted newest-first, and the dataset is continuously ingested. Both facts together mean offset paging over a live index is not a consistent snapshot: if 30 new jobs are indexed while you're on page 3, the rows that were on page 3 shift to page 4, and you'll see 30 rows twice — while rows that fall off the far end are missed entirely.

This is inherent to offset paging, not specific to this API — and it's what the cursor fixes. A keyset cursor pins your position to the last row you saw, so new rows arriving above you can't shift the pages below. In order of preference:

  1. Use the cursor

    For any walk longer than a page or two, follow next_cursor. No drift, no depth limit, no duplicates from ingestion racing your walk.

  2. If you must offset-page, deduplicate by id

    Collapsed results have no cursor, so offset paging is sometimes unavoidable. Collect into a Map keyed by job.id rather than an array — costs nothing, fixes double-counting, doesn't fix misses.

  3. Sync with the feed instead

    For anything that needs completeness — keeping a local mirror, driving alerts — don't walk the index at all. Take one full snapshot, then poll /v1/jobs/feed with a cutoff. Syncing the feed has the pattern.

Counting without fetching#

Counting has its own endpoint now: /v1/jobs/count takes the same filters as /v1/jobs and returns just the number.

BASH
curl -H "Authorization: Bearer $HYPERJOBS_KEY" \
  "https://api.hyperjobs.io/v1/jobs/count?skill=rust&remote=1"
JSON
{ "count": 1284 }

Counts are an exact COUNT(*) over the same filters, not an estimate — which also means a very broad query pays to count rows it will never return. If you only need "are there any", /v1/jobs?limit=1 and check data.length.

Sorting#

Not configurable. /v1/jobs is always newest-first by posted_at (falling back to created_at when a posting carries no publish date), with id as the tiebreak. The feeds (/feed, /expired, /modified) are the opposite — ascending by index, expiry, and modification time respectively, so a cursor walk drains them oldest-first. /v1/companies is always ordered by data tier, then follower count.

If you need a different order, fetch the filtered set and sort client-side — with limit=200 and a tight filter that's usually one or two requests.

Was this page helpful?