DEV Community

Gio Rich
Gio Rich

Posted on

One API for every company's job board (Greenhouse, Lever, Ashby, Workday, Workable...) with Python or Node

I built this actor; it's a paid tool on Apify with a free trial credit.

Most companies don't host their own job listings. The careers page is a thin wrapper around an applicant tracking system: Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor or BambooHR. Each of those publishes the board as a public JSON or XML feed, which is great, except that there are ten of them, every one with a different schema, pagination rule and date format, and half the time you don't even know which one a company uses.

I'm an 18-year-old engineering student, and I wrapped all ten behind one input and one output schema in an actor called Job Boards Scraper. This post shows how to call it from Python and Node, what the rows look like, and a small project: a daily "who started hiring for X" feed for sales prospecting.

How it works, briefly

You give it a list of entries. Each one can be an ATS board URL (boards.greenhouse.io/stripe, jobs.lever.co/palantir, jobs.ashbyhq.com/notion, nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite ...), a company careers page (notion.com/careers, where the embedded board is detected from the page) or just a company website or name (stripe.com, blueground), which is tried on every platform until a board with open jobs answers. Every job is then mapped to one schema: title, company, location (plus city, region, country), remote flag, department, team, employment type, salary range when published, posting date, full description as plain text, and the job and apply URLs. No browser, no scraping of rendered pages, just the platforms' own public endpoints over HTTP, so a 700-job board takes a couple of seconds.

Python example

pip install apify-client
Enter fullscreen mode Exit fullscreen mode
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("rel8ble/ats-job-boards-scraper").call(run_input={
    "boards": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.ashbyhq.com/notion",
        "https://www.notion.com/careers",   # careers page: board detected
        "blueground",                        # bare company name: guessed
    ],
    "titleKeywords": ["engineer", "developer"],
    "locationKeywords": ["remote"],
    "maxJobsPerBoard": 100,
})

for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["company"], "|", job["title"], "|", job["location"], "|", job["publishedAt"][:10], "|", job["url"])
Enter fullscreen mode Exit fullscreen mode

Output (shortened):

Stripe | Software Engineer, Infrastructure | Remote, US | 2026-09-24 | https://stripe.com/jobs/search?gh_jid=...
Notion | Software Engineer, Developer Platform | San Francisco, California | 2026-08-24 | https://jobs.ashbyhq.com/notion/1fc309c8-...
Enter fullscreen mode Exit fullscreen mode

Node example

npm install apify-client
Enter fullscreen mode Exit fullscreen mode
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('rel8ble/ats-job-boards-scraper').call({
    boards: ['https://jobs.lever.co/palantir', 'https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite'],
    titleKeywords: ['sales', 'account executive'],
    includeDescription: false,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, 'jobs');
console.log(items[0]);
Enter fullscreen mode Exit fullscreen mode

What a row looks like

{
    "id": "1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
    "title": "Software Engineer, Developer Platform",
    "company": "Notion",
    "ats": "ashby",
    "url": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
    "applyUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a/application",
    "location": "San Francisco, California",
    "locations": ["San Francisco, California", "New York, New York"],
    "city": "San Francisco",
    "region": "California",
    "country": "United States",
    "workplaceType": "hybrid",
    "isRemote": true,
    "department": "Engineering",
    "team": "Engineering",
    "employmentType": "full-time",
    "salaryMin": 213000,
    "salaryMax": 320000,
    "salaryCurrency": "USD",
    "salaryText": "$213,000 - $320,000",
    "publishedAt": "2026-08-24T14:44:49.699Z",
    "description": "WHO WE ARE\n\nNotion is the collaborative AI workspace ...",
    "boardUrl": "https://jobs.ashbyhq.com/notion",
    "scrapedAt": "2026-10-08T17:45:45.963Z"
}
Enter fullscreen mode Exit fullscreen mode

Small project: a daily hiring-signal feed

The classic sales play: a company that just opened three SDR roles is about to buy sales tooling; a company hiring a "Head of Data" is about to buy data infrastructure. Here's the whole thing in 40 lines. It keeps a JSON file of job ids it has seen and prints only the new ones.

import json, os
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
SEEN = "seen_jobs.json"
seen = set(json.load(open(SEEN))) if os.path.exists(SEEN) else set()

accounts = ["stripe", "notion", "https://jobs.lever.co/palantir", "bunq.com", "blueground"]

run = client.actor("rel8ble/ats-job-boards-scraper").call(run_input={
    "boards": accounts,
    "titleKeywords": ["sales development", "sdr", "account executive", "revenue operations"],
    "includeDescription": False,
})

new = []
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    key = f'{job["ats"]}:{job["id"]}'
    if key in seen:
        continue
    seen.add(key)
    new.append(job)

for job in new:
    print(f'NEW  {job["company"]:<12} {job["title"]:<50} {job["location"] or "-":<30} {job["url"]}')

json.dump(sorted(seen), open(SEEN, "w"))
print(f"{len(new)} new postings")
Enter fullscreen mode Exit fullscreen mode

Run it from cron or an Apify schedule and pipe the output into Slack, a CRM task or a spreadsheet. The run costs $1 per 1,000 jobs, so a 500-account list with filtering costs a few cents a day.

Things to know

  • Ten platforms, not all of them. iCIMS, Taleo, SuccessFactors, Jobvite and fully custom career sites are reported as unresolved in the run summary rather than scraped.
  • Descriptions are free on Greenhouse, Lever, Ashby, Recruitee, Personio and Teamtailor. On SmartRecruiters, Workday, Workable and BambooHR they cost one extra request per job (still one result); set includeDescription: false for a fast count.
  • Company names come from the platform where it publishes one; for Lever, Ashby and Workday they're derived from the board slug (notion -> "Notion").
  • Dates: Workday only says "Posted 3 Days Ago", which is converted to a date; "Posted 30+ Days Ago" stays null.
  • Salary is a structured field on Lever, Ashby, Recruitee and Teamtailor; elsewhere it's parsed from the description when a range like "$120,000 - $150,000" is stated.

If something breaks or a field is wrong, the Issues tab on the actor page reaches me directly.

Job Boards Scraper on Apify


This article was drafted with AI and published by me.

Top comments (0)