Articles
Dec 3, 2025 - 8 MIN READ
Building Jobven: Designing a Developer-Friendly API

Building Jobven: Designing a Developer-Friendly API

How I designed Jobven's public API with cursor-based pagination, flexible filtering, and incremental sync - decisions that make integration straightforward for developers.

Peter Oliha

Peter Oliha

When I started building Jobven, the data collection was the hard part - scraping employer career pages, normalizing job data, keeping everything fresh. But the API that developers would actually use? That needed to be simple.

I've integrated enough poorly-designed APIs to know what frustrates developers. This is how I approached designing Jobven's public API and the decisions behind it.

Start With the Response Shape

Before writing any endpoint code, I mocked out what I wanted the response to look like. A developer should be able to glance at a response and immediately understand the data:

{
  "data": [
    {
      "id": "abc123-def456-ghi789",
      "title": "Frontend Developer",
      "summary": "Build beautiful user interfaces for our SaaS platform",
      "companies": [
        {
          "id": 123,
          "name": "TechStartup Inc",
          "website": "https://techstartup.com"
        }
      ],
      "locations": [
        {
          "addressLocality": "Austin",
          "addressRegion": "Texas",
          "addressCountry": "US",
          "workLocation": "hybrid"
        }
      ],
      "remoteType": "hybrid",
      "employmentType": "full_time",
      "experienceLevel": "mid",
      "salary": {
        "min": 90000,
        "max": 120000,
        "currency": "USD",
        "period": "yearly"
      },
      "skills": {
        "primary_skills": ["React", "TypeScript", "CSS"],
        "secondary_skills": ["Node.js", "GraphQL"],
        "soft_skills": ["communication", "teamwork"]
      },
      "postedAt": 1733097600000,
      "status": "active"
    }
  ],
  "meta": {
    "total": 15234,
    "count": 5,
    "nextCursor": "eyJpZCI6ImFiYzEyMyJ9",
    "hasMore": true,
    "requestTimeMs": 45
  }
}

A few deliberate choices here:

Data and meta separation - The actual jobs are in data, pagination info is in meta. Clean separation that every modern API follows.

Arrays for companies and locations - Some jobs have multiple hiring companies or locations. Using arrays from the start means developers don't need to handle both single values and arrays.

Nested skills object - Rather than a flat array, skills are categorized. This took extra work on my end to classify, but it makes the data more useful for filtering and display.

Unix timestamps - postedAt is a millisecond timestamp. No timezone ambiguity, easy to work with in any language.

Authentication: Keep It Simple

I went with API key authentication via header:

curl -X GET 'https://api.jobven.com/v1/public/jobs?limit=5' \
  -H 'X-API-Key: your_api_key'

OAuth would be overkill for a data API. JWT adds complexity. A simple API key in the X-API-Key header is:

  • Easy to generate and revoke from a dashboard
  • Works identically in every HTTP client
  • No token refresh flows to implement

The key identifies the account, enforces rate limits, and tracks usage. That's all I need.

Filtering: Be Generous

The filtering system was where I spent the most design time. Developers building job boards need precise control over what jobs they fetch.

The q parameter searches across title, summary, and description:

curl -X GET 'https://api.jobven.com/v1/public/jobs?q=react+developer&limit=20' \
  -H 'X-API-Key: your_api_key'

Remote Type

Remote work is a first-class filter, not buried in location:

# Options: remote, hybrid, onsite, flexible
curl -X GET 'https://api.jobven.com/v1/public/jobs?remoteType[]=remote' \
  -H 'X-API-Key: your_api_key'

The array syntax (remoteType[]) lets you filter for multiple types in one request.

Location

Filter by country, region, or city:

curl -X GET 'https://api.jobven.com/v1/public/jobs?country=US&location=San+Francisco' \
  -H 'X-API-Key: your_api_key'

Skills

Comma-separated skill filtering:

curl -X GET 'https://api.jobven.com/v1/public/jobs?skills=python,aws,kubernetes' \
  -H 'X-API-Key: your_api_key'

This returns jobs matching any of these skills. I considered adding skillsMatch=all for AND logic, but kept it simple for v1.

Experience Level

# Options: entry, mid, senior, lead, executive
curl -X GET 'https://api.jobven.com/v1/public/jobs?experienceLevel=senior' \
  -H 'X-API-Key: your_api_key'

Combining Filters

All filters can be combined:

const params = new URLSearchParams({
  q: 'backend engineer',
  'remoteType[]': 'remote',
  skills: 'python,postgresql',
  experienceLevel: 'mid',
  limit: '25'
});

const response = await fetch(
  `https://api.jobven.com/v1/public/jobs?${params}`,
  {
    headers: { 'X-API-Key': process.env.JOBVEN_API_KEY }
  }
);

Pagination: Cursors Over Offsets

I chose cursor-based pagination over offset-based. Here's why:

Offset pagination breaks with changing data. If you're on page 2 and new jobs get added, page 3 might include duplicates or skip items. With thousands of jobs being added and removed daily, this matters.

Cursor pagination is consistent. The cursor encodes where you left off, so you always get the next set of results regardless of what changed.

async function getAllJobs(filters = {}) {
  const jobs = [];
  let cursor = null;
  let hasMore = true;

  while (hasMore) {
    const params = new URLSearchParams({
      ...filters,
      limit: '100',
      ...(cursor && { cursor })
    });

    const response = await fetch(
      `https://api.jobven.com/v1/public/jobs?${params}`,
      {
        headers: { 'X-API-Key': process.env.JOBVEN_API_KEY }
      }
    );

    const data = await response.json();
    jobs.push(...data.data);

    cursor = data.meta.nextCursor;
    hasMore = data.meta.hasMore;

    console.log(`Fetched ${jobs.length} of ${data.meta.total} jobs...`);
  }

  return jobs;
}

The meta object tells you everything: total count, whether there's more data, and the cursor for the next page.

Incremental Sync: The Feature That Matters Most

For production applications, fetching all jobs every time is wasteful. The postedAfter parameter enables incremental sync:

async function syncNewJobs(lastSyncTimestamp) {
  const params = new URLSearchParams({
    postedAfter: lastSyncTimestamp.toString(),
    limit: '100'
  });

  const response = await fetch(
    `https://api.jobven.com/v1/public/jobs?${params}`,
    {
      headers: { 'X-API-Key': process.env.JOBVEN_API_KEY }
    }
  );

  const { data: newJobs, meta } = await response.json();

  // Save to your database
  for (const job of newJobs) {
    await db.jobs.upsert({
      where: { externalId: job.id },
      create: {
        externalId: job.id,
        title: job.title,
        companyName: job.companies[0]?.name,
        remoteType: job.remoteType,
        postedAt: new Date(job.postedAt)
      },
      update: {
        title: job.title,
        status: job.status
      }
    });
  }

  return newJobs.length;
}

// Run daily - fetch jobs posted in the last 24 hours
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
const newJobCount = await syncNewJobs(oneDayAgo);

This is the feature that makes Jobven practical for real applications. A daily cron job that syncs only new postings uses minimal API calls and keeps your data fresh.

Rate Limits: Transparent and Tiered

Rate limiting is necessary, but it shouldn't be a mystery. Every response includes headers:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 1733184000

And the limits are published clearly:

TierCalls/MonthCalls/DayCalls/Second
Free750251
Starter10,0003345
Growth30,0001,0005
Professional100,0003,33410

The free tier is deliberately generous enough to build something real. I want developers to hit the API, build features, and upgrade when they have actual traffic.

Error Handling: Be Helpful

When something goes wrong, the error response should explain what happened:

async function fetchJobs(params) {
  const response = await fetch(
    `https://api.jobven.com/v1/public/jobs?${params}`,
    {
      headers: { 'X-API-Key': process.env.JOBVEN_API_KEY }
    }
  );

  if (!response.ok) {
    if (response.status === 401) {
      throw new Error('Invalid API key');
    }
    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After') || 1;
      throw new Error(`Rate limit exceeded - retry after ${retryAfter}s`);
    }
    throw new Error(`API error: ${response.status}`);
  }

  return await response.json();
}

Standard HTTP status codes (401, 429, etc.) plus descriptive error bodies. No proprietary error code systems to memorize.

Documentation: Examples in Every Language

API documentation without code examples is incomplete. Every endpoint in Jobven's docs includes examples in cURL, JavaScript, and Python at minimum:

import os
import requests

response = requests.get(
    'https://api.jobven.com/v1/public/jobs',
    headers={'X-API-Key': os.environ['JOBVEN_API_KEY']},
    params={
        'q': 'backend engineer',
        'remoteType': 'remote',
        'skills': 'python,postgresql',
        'experienceLevel': 'mid',
        'limit': 25
    }
)

data = response.json()
print(f"Found {data['meta']['total']} matching jobs")

Copy-paste examples that actually work. No placeholder variables that need to be figured out.

What I'd Do Differently

A few things I've noted for future iterations:

GraphQL option - Some developers would prefer fetching only the fields they need. REST is simpler to start, but GraphQL might be worth adding.

Webhook support - Instead of polling with postedAfter, let developers subscribe to new job events. On the roadmap.

SDK packages - Official npm and PyPI packages would reduce boilerplate. Currently deprioritized since the REST API is simple enough.

Try It

The API is live with a free tier. If you're building something with job data, give it a try:

The best feedback comes from developers actually using it. Let me know what works and what doesn't.

Copyright © 2026