Changes feed
An ordered log of every change to the catalogue. Use it to keep your own copy of the jobs current without re-downloading everything.
GET/v1/changes
Parameters
| Parameter | Type | Description |
|---|---|---|
| since | string | Return changes after this position. Start with 0, then pass the next_since you stored. |
| limit | integer | Changes per batch, 1–1000. Default 500. |
| description | string | none (default), text, html or both. Not included for expired jobs. |
| source_fields | boolean | Included by default; pass false for smaller batches. |
Response
json
{
"changes": [
{ "seq": "1201", "kind": "created", "at": "2026-10-11T14:02:11.000Z", "job": { /* job object */ } },
{ "seq": "1202", "kind": "expired", "at": "2026-10-11T14:05:40.000Z", "job": { "id": 88, "status": "expired", … } }
],
"next_since": "1202",
"more": true
}| Parameter | Type | Description |
|---|---|---|
| kind | created | A new job was collected. |
| kind | updated | The job's content changed. Replace your copy. |
| kind | expired | The job closed. It was missing from two complete scans of its source at least two hours apart. |
| kind | excluded | The job is no longer eligible for this feed (for example, a location turned out to be outside Canada). It did not necessarily close. |
Sync loop
JavaScript
let since = await loadCheckpoint() ?? "0";
for (;;) {
const response = await fetch(`https://api.thejobsapi.com/v1/changes?since=${since}&limit=1000`, {
headers: { Authorization: `Bearer ${process.env.THEJOBSAPI_KEY}` },
});
if (!response.ok) throw new Error((await response.json()).error);
const batch = await response.json();
for (const change of batch.changes) {
if (change.kind === "expired" || change.kind === "excluded") await removeJob(change.job.id);
else await upsertJob(change.job);
}
since = batch.next_since;
await saveCheckpoint(since); // save after each batch
if (!batch.more) break; // caught up; poll again later
}Poll every few minutes. Every change event in a batch counts as one job toward your allowance.