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 listedAfter fetches 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

  1. First sync: fetch the jobs you need (or start from a recent date)
  2. Store the newest listedAt you received
  3. Next sync: send listedAfter = that value minus 5 minutes, sorted by listedAt ascending
  4. 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 listedAt from 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. listedAfter finds new and reopened jobs. To learn when a job closes, subscribe to the job.closed webhook.

Next Steps

Fetch All Jobs

How pagination works when fetching multiple pages.

Filter Jobs

Combine incremental sync with filters.

Handle Rate Limits

Stay within your plan's limits while syncing.