Sync Only New Jobs
Fetch only the jobs listed since your last sync with listedAfter, or receive them as they are listed with webhooks.
There are two ways to keep a copy of Jobven's jobs current:
- Webhooks push each job to you as it is listed, updated or closed. Nothing to schedule. Set up webhooks
- Pull with
listedAfterfetches the jobs listed since your last run, on your own schedule.
This guide covers the second.
How It Works
Every job carries listedAt: when Jobven made it available, in Unix seconds. A job that closes and later reopens is listed again, with a new listedAt. listedAfter returns the jobs listed at or after a time:
curl -H "X-API-Key: $JOBVEN_API_KEY" \
"https://api.jobven.com/v1/public/jobs?listedAfter=1735257600&sortBy=listedAt&order=ASC"
Use listedAfter to sync, not postedAfter. postedAt is the employer's posting date, usually hours before the job reaches Jobven, so a sync on postedAfter skips every job posted before your last run but listed after it. postedAfter is for asking for jobs posted in a period.
The Pattern
- First sync: fetch the jobs you need (or start from a recent date)
- Store the newest
listedAtyou received - Next sync: send
listedAfter= that value minus 5 minutes, sorted bylistedAtascending - Upsert by
id: the 5-minute overlap returns some jobs again, and a job saved twice must not become two
Examples
JavaScript
import fs from 'fs';
const STATE_FILE = './last_listed_at.txt';
const OVERLAP_SECONDS = 5 * 60;
function getLastListedAt() {
try {
return parseInt(fs.readFileSync(STATE_FILE, 'utf8'));
} catch {
// First run: start from 7 days ago
return Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60;
}
}
async function syncNewJobs() {
const lastListedAt = getLastListedAt();
let newest = lastListedAt;
const jobs = [];
let cursor = null;
do {
const params = new URLSearchParams({
listedAfter: String(lastListedAt - OVERLAP_SECONDS),
sortBy: 'listedAt',
order: 'ASC',
limit: '100',
});
if (cursor) params.set('cursor', cursor);
const response = await fetch(
`https://api.jobven.com/v1/public/jobs?${params}`,
{ headers: { 'X-API-Key': process.env.JOBVEN_API_KEY } }
);
if (!response.ok) throw new Error(`Jobven returned ${response.status}`);
const { data, meta } = await response.json();
for (const job of data) {
jobs.push(job);
newest = Math.max(newest, job.listedAt);
}
cursor = meta.nextCursor;
} while (cursor);
// Upsert each job by id here (save to database, etc.)
// Only after the jobs are saved
fs.writeFileSync(STATE_FILE, String(newest));
console.log(`Synced ${jobs.length} jobs`);
return jobs;
}
Python
import os
import time
from pathlib import Path
import requests
STATE_FILE = Path('./last_listed_at.txt')
OVERLAP_SECONDS = 5 * 60
def get_last_listed_at():
try:
return int(STATE_FILE.read_text())
except (FileNotFoundError, ValueError):
# First run: start from 7 days ago
return int(time.time()) - 7 * 24 * 60 * 60
def sync_new_jobs():
last_listed_at = get_last_listed_at()
newest = last_listed_at
jobs = []
cursor = None
while True:
params = {
'listedAfter': last_listed_at - OVERLAP_SECONDS,
'sortBy': 'listedAt',
'order': 'ASC',
'limit': 100,
}
if cursor:
params['cursor'] = cursor
response = requests.get(
'https://api.jobven.com/v1/public/jobs',
headers={'X-API-Key': os.environ['JOBVEN_API_KEY']},
params=params,
)
response.raise_for_status()
data = response.json()
for job in data['data']:
jobs.append(job)
newest = max(newest, job['listedAt'])
cursor = data['meta'].get('nextCursor')
if not cursor:
break
# Upsert each job by id here (save to database, etc.)
# Only after the jobs are saved
STATE_FILE.write_text(str(newest))
print(f"Synced {len(jobs)} jobs")
return jobs
Tips
- Store
listedAtfrom the jobs, not your own clock. It comes from Jobven, so a slow run or a clock difference cannot open a gap. - Keep the overlap. A job can be saved on Jobven's side a moment after another job with a later
listedAt; the 5 minutes cover it. - Don't advance the stored value if the sync fails, so the next run retries from the same point.
- Closures don't appear here.
listedAfterfinds new and reopened jobs. To learn when a job closes, subscribe to thejob.closedwebhook.