DEV Community

Kunnu Data
Kunnu Data

Posted on

Workable has a public search API across every employer — here's how to use it

If you want job-market data — who is hiring for a role, where, and whether it's remote — most tutorials tell you to scrape HTML, drive a headless browser, or hit one company's careers page at a time. For Workable, the applicant tracking system (ATS) that tens of thousands of employers use, you don't need any of that. Workable runs a public, unauthenticated JSON search API at jobs.workable.com that queries every employer at once. No login, no proxies, no HTML parsing.

This post covers:

  1. the two public Workable endpoints (one company vs. every company),
  2. the search API in detail — parameters, pagination, and the fields you get back,
  3. a copy-paste Node function that pages through results,
  4. the gotchas, and what you can build with it.

Two public endpoints

Workable exposes two different public JSON endpoints, and they're good at different things:

Goal Endpoint
One known employer's open jobs https://apply.workable.com/api/v1/widget/accounts/{slug}?details=true
Keyword search across all employers https://jobs.workable.com/api/v1/jobs?query=...

The first is the per-company widget feed. The slug is a company's Workable handle — e.g. huggingface from apply.workable.com/huggingface/. It returns that one employer's postings with rich per-job detail (department, function, employment type, telecommuting).

The second — the focus of this post — is the cross-employer search. Give it a keyword and it returns matching jobs from across Workable's network, each carrying its own company info, including the company website, which is gold for enrichment.

The search API

It's a plain GET request with these query parameters:

Param Meaning Example
query keyword (matches title/description) data engineer
location free-text location match Berlin, Australia
workplace filter by work setting remote
day_range posted within the last N days 7
pageToken pagination cursor (from the previous response) —
https://jobs.workable.com/api/v1/jobs?query=data+engineer&day_range=7&workplace=remote
Enter fullscreen mode Exit fullscreen mode

The response is { "jobs": [ ... ], "nextPageToken": "..." }. You get about 20 jobs per page; keep following nextPageToken until it's gone. Each job looks like this (a real result, fetched October 2026):

{
  "id": "bb0dc9aa-d21a-46af-9173-516bda550e00",
  "title": "Data Engineer",
  "company": "Reliance Health",
  "website": "http://getreliancehealth.com",
  "department": "Engineering",
  "employmentType": "Full-time",
  "workplace": "remote",
  "location": "Lagos, Nigeria",
  "posted": "2026-10-20T08:44:59.775Z",
  "url": "https://jobs.workable.com/view/p6GnxfhZvENExQam1Mpjcj/remote-data-engineer-in-lagos-at-reliance-health"
}
Enter fullscreen mode Exit fullscreen mode

The raw payload carries more — description, requirementsSection, benefitsSection, locations, updated, company.image — but these are the fields most people actually want.

Page through it (Node, no dependencies)

// Search every Workable employer for a keyword. Public endpoint, no auth.
async function* searchWorkable(query, { location = '', remoteOnly = false, dayRange = 0, max = 200 } = {}) {
  let pageToken = null;
  let seen = 0;
  do {
    const p = new URLSearchParams({ query });
    if (location) p.set('location', location);
    if (remoteOnly) p.set('workplace', 'remote');
    if (dayRange) p.set('day_range', String(dayRange));
    if (pageToken) p.set('pageToken', pageToken);

    const res = await fetch(`https://jobs.workable.com/api/v1/jobs?${p}`, {
      headers: { accept: 'application/json' },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();

    for (const j of data.jobs ?? []) {
      if (seen++ >= max) return;
      yield {
        id: j.id,
        title: j.title,
        company: j.company?.title,
        website: j.company?.website,
        department: j.department,
        workplace: j.workplace,           // remote | hybrid | on_site
        location: [j.location?.city, j.location?.countryName].filter(Boolean).join(', '),
        posted: j.created,
        url: j.url,
      };
    }
    pageToken = data.nextPageToken ?? null;
  } while (pageToken && seen < max);
}

// Example: remote data-engineering roles posted in the last 7 days
for await (const job of searchWorkable('data engineer', { remoteOnly: true, dayRange: 7, max: 60 })) {
  console.log(`${job.company} — ${job.title} (${job.location}) ${job.url}`);
}
Enter fullscreen mode Exit fullscreen mode

That's the whole thing: an async generator that yields normalized jobs and stops at max. No API key, no browser, no proxy.

Gotchas (from running this at scale)

  • Paginate with nextPageToken, not a page number. About 20 results per page; stop when jobs is empty or the token is gone.
  • day_range is your recency filter. Set it to 1–7 and you've got a "what's new this week" monitor — store the IDs and diff between runs to get newly posted and newly closed roles.
  • company.website is the enrichment key. The cross-employer search hands you each job's company domain directly; the per-company widget endpoint does not. If you're building a lead list, start here.
  • No salary field. Neither endpoint returns structured pay. Parse it from the description if you need it, and require an explicit currency symbol so "5–7 years" doesn't get read as a salary.
  • workplace is remote / hybrid / on_site. The workplace=remote query filter maps to the first.
  • The two endpoints disagree on IDs. The search uses a UUID id; the per-company widget uses a shortcode. If you join data from both, key on the job URL, not the ID.
  • Be polite. It's public, but it isn't yours: add a small delay between pages, cache responses, and read only publicly posted listings — no personal contact data.

What you can build

  • A hiring monitor — "new remote data-engineering roles, last 7 days," in your inbox each morning. day_range=7 plus an ID diff is the whole trick.
  • A lead list — companies hiring for a role, deduped by company.website, ready to enrich. Five open backend roles in a month is a strong buying signal for dev tooling.
  • Market tracking — counts by location and role over time, straight from the created timestamps.

Don't want to maintain it?

I packaged the above — pagination, normalization, both the search and per-company endpoints, and export to CSV / JSON / Google Sheets — as an Apify actor: Workable Jobs Scraper. Give it a keyword or a company; get clean rows. If you want the same across eight ATS platforms at once with company-level hiring signals (velocity, function mix, tech stack), that's Hiring Signals.

Both read only publicly published job postings — no logins, no personal contact data.

Written with AI assistance; the endpoints and output were verified against live data before publishing.

Top comments (0)