API DOCUMENTATION / RECOMMENDED INGESTION STRATEGY

Recommended ingestion strategy

Load once, then sync with the changes feed: polling, catching up after an outage, and closed jobs.

The short version

  1. Load the jobs you want once with GET /v1/jobs.
  2. From then on, read only what changed with GET /v1/jobs/changes.
  3. Store the resume_cursor after every page you save, and write each job by its job_id so that reading an event twice does no harm.

1. First load

Note the time, then page through GET /v1/jobs with your filters and limit=250, passing each next_cursor back as cursor. Without a search term the list is sorted by job_updated_at, newest first. Save each job under its job_id.

GET https://api.headcountlabs.com/v1/jobs?country=US&limit=250
GET https://api.headcountlabs.com/v1/jobs?country=US&limit=250&cursor=<next_cursor>

Every page of one listing reads the same catalog snapshot (snapshot_watermark). If a new snapshot is published during your load, the next cursor returns 400. Start that listing again from the first page. Jobs you already saved are simply written again, but each job returned still counts toward your plan, so split a large load into smaller slices (for example one country, or one updated_since / updated_until range, at a time). Then a restart only repeats one slice.

2. Incremental sync with the changes feed

Start the feed with since set to the time you began the first load, so nothing that changed during the load is missed. After that, pass the resume_cursor from each response as after. Keep reading while has_more is true.

GET https://api.headcountlabs.com/v1/jobs/changes?since=2026-10-07T00:00:00Z&limit=1000
GET https://api.headcountlabs.com/v1/jobs/changes?after=<resume_cursor>&limit=1000

Each event has event_id, job_id, type (created, updated or expired) and occurred_at. Created and updated events carry the full job; replace your copy with it. Events come in order. types limits the feed to some event types and company_id to one company. Each event counts as one record.

3. How often to poll

Every job is re-checked daily, so polling the changes feed every 15 to 60 minutes is enough for most products. A poll that finds no events uses one request and no jobs. Your request window (30 or 60 requests a minute, see rate limits) is far above what polling needs.

4. Catching up after an outage

Keep the last resume_cursor you stored and continue with after; the feed returns every event since that point, in order, so there is no gap.

A cursor from an older catalog snapshot returns 400. Then restart with since set a little before the occurred_at of the last event you saved, because since returns events strictly after that time. Skip events whose event_id you already applied. Because you write by job_id, an event applied twice leaves the same result, so there are no duplicates.

On 503 with service_unavailable (the catalog changed during the request) or catalog_unavailable, retry shortly. On 429, wait for Retry-After.

5. How closed jobs are signalled

An expired event means the job left the open set: every posting of it is no longer listed, or it was withdrawn, or it was merged into another job. Expired events carry no job object. Mark the job closed in your copy.

If you need the reason, call GET /v1/jobs/{job_id}: 308 means it merged into the job in merged_into_job_id, and 410 with job_withdrawn means it was withdrawn. On each job, currentness (current, uncertain, not_current), each posting's posting_status (listed or no_longer_listed), latest_application_check and the stale flag tell you how open it is.

List and count routes return only current jobs unless you pass currentness or active.

Checklist

  • Write jobs by job_id; never append blindly.
  • Store resume_cursor only after the page it came with is saved.
  • Keep the occurred_at and event_id of the last applied event as a fallback.
  • Handle 400 (restart), 429 (wait) and 503 (retry).
  • Pick the right date filter for the first load; see which date to filter on.

Continue in the reference

Routes, filters and response fields.

API reference Job filters