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:
- the two public Workable endpoints (one company vs. every company),
- the search API in detail — parameters, pagination, and the fields you get back,
- a copy-paste Node function that pages through results,
- 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
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"
}
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}`);
}
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 whenjobsis empty or the token is gone. -
day_rangeis 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.websiteis 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.
-
workplaceisremote/hybrid/on_site. Theworkplace=remotequery filter maps to the first. -
The two endpoints disagree on IDs. The search uses a UUID
id; the per-company widget uses ashortcode. 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=7plus 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
createdtimestamps.
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)