Disclosure: I'm the maker of the ATS Jobs Scraper on the Apify Store used below. You can also call each ATS's public API yourself; this post shows both the problem and the shortcut, with real output.
Most tech companies don't host their own job listings. Their careers page is a front end for an applicant tracking system (ATS): Greenhouse, Lever, Ashby, Workable or SmartRecruiters. Each one has a public job-board API, which sounds easy until you need more than one company:
- every ATS has a different URL scheme, JSON shape, pagination and date format,
- salaries hide in different places (a structured field on Ashby, free text in the description on Greenhouse),
- you have to work out which ATS a company uses before you can call anything,
- and for alerts you need to remember which jobs you've already seen.
This guide gets open jobs from all of them in one HTTP call and one flat schema, from curl or Python, and then turns it into a daily "new jobs" alert.
How to tell which ATS a company uses
Click Apply on any job on the company's careers page and look at the link:
- Greenhouse:
boards.greenhouse.io/stripeorjob-boards.greenhouse.io/stripe(slugstripe) - Lever:
jobs.lever.co/spotifyorjobs.eu.lever.co/...(slugspotify) - Ashby:
jobs.ashbyhq.com/notion(slugnotion) - Workable:
apply.workable.com/huggingface(slughuggingface) - SmartRecruiters:
jobs.smartrecruiters.com/ServiceNow(slugServiceNow)
You can paste those links as they are. If you only know the company name, a bare slug such as airbnb also works: the Actor tries each ATS and uses the one that answers.
The one-call version (curl)
Apify Actors can be called synchronously: POST the input, get the dataset back in the same response.
curl -X POST \
"https://api.apify.com/v2/acts/scrapemole~ats-jobs-scraper/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companies": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/spotify", "https://jobs.ashbyhq.com/notion"], "keywords": ["engineer"], "maxJobsPerCompany": 2, "includeDescription": false}'
You get a free token by signing up at Apify (the free plan includes monthly credit). That call took about 5 seconds and returned 6 jobs, two per company, in the same shape whichever ATS they came from. Trimmed, from a real run on 24 October 2026:
[
{
"status": "ok",
"ats": "ashby",
"companyName": "Notion",
"title": "Software Engineer, Developer Platform",
"url": "https://jobs.ashbyhq.com/notion/8c0bf773-e536-45c9-9a6c-8ee478148cc8",
"location": "San Francisco, California",
"workplaceType": "hybrid",
"department": "Engineering",
"postedAt": "2026-10-19T17:28:06.956Z",
"salaryMin": 179000,
"salaryMax": 200000,
"salaryCurrency": "USD",
"salaryInterval": "year",
"salaryText": "$179,000 – $200,000"
},
{
"status": "ok",
"ats": "lever",
"companyName": "Spotify",
"title": "Machine Learning Engineer - Subscriptions",
"url": "https://jobs.lever.co/spotify/19c12e29-b6d4-4d97-9053-48238fb7c381",
"location": "London",
"workplaceType": "hybrid",
"department": "Engineering",
"postedAt": "2026-10-23T16:53:57.686Z",
"salaryMin": null
},
{
"status": "ok",
"ats": "greenhouse",
"companyName": "Stripe",
"title": "Staff Software Engineer, Payments Intelligence",
"url": "https://stripe.com/jobs/search?gh_jid=8260980",
"location": "Seattle, New York, San Francisco",
"postedAt": "2026-10-22T19:52:24.000Z",
"salaryMin": null
}
]
salaryMin and salaryMax are filled whenever the company publishes pay, either in the ATS's own field or in the description text. Set includeDescription to true (the default) to also get the job description as plain text, with e-mail addresses removed.
Python: many companies into a CSV
No SDK needed, just requests. This is the exact script I ran:
# pip install requests
import csv, os, requests
URL = "https://api.apify.com/v2/acts/scrapemole~ats-jobs-scraper/run-sync-get-dataset-items"
HEADERS = {"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"}
payload = {
"companies": [
"https://boards.greenhouse.io/stripe", # Greenhouse
"https://jobs.lever.co/spotify", # Lever
"https://jobs.ashbyhq.com/notion", # Ashby
"airbnb", # bare slug: the ATS is detected for you
"not-a-real-company-xyz",
],
"keywords": ["engineer"],
"maxJobsPerCompany": 20,
"includeDescription": False,
}
rows = requests.post(URL, json=payload, headers=HEADERS, timeout=300).json()
jobs = [r for r in rows if r["status"] == "ok"]
errors = [r for r in rows if r["status"] != "ok"]
with open("jobs.csv", "w", newline="") as f:
cols = ["companyName", "ats", "title", "location", "workplaceType",
"salaryMin", "salaryMax", "salaryCurrency", "postedAt", "url"]
w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore")
w.writeheader()
w.writerows(jobs)
print(f"{len(jobs)} jobs saved to jobs.csv")
for r in errors:
print("skipped:", r.get("input") or r.get("company"), r["errorCode"], r["errorMessage"])
Output:
80 jobs saved to jobs.csv
skipped: not-a-real-company-xyz NOT_FOUND No Greenhouse, Lever, Ashby, Workable or SmartRecruiters job board found for "not-a-real-company-xyz". Paste the careers-page link instead.
80 engineering jobs from four companies on three different ATSs in about 5 seconds, 36 of them with a parsed salary range. The made-up company comes back as a clear NOT_FOUND row instead of crashing the run, and it isn't charged. If you prefer the official client, pip install apify-client and call client.actor("scrapemole/ats-jobs-scraper").call(run_input=payload); the rows are the same.
Useful filters, all optional: keywords and excludeKeywords (matched against the title), locations (e.g. ["New York", "Remote"]), remoteOnly, postedWithinDays and maxJobsPerCompany.
A daily "new jobs" alert
Add "onlyNew": true and a stateKey (any name for this saved search). The Actor remembers which job IDs it already gave you, per company, in a key-value store on your own Apify account:
curl -X POST \
"https://api.apify.com/v2/acts/scrapemole~ats-jobs-scraper/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companies": ["notion", "airbnb"], "keywords": ["engineer"], "onlyNew": true, "stateKey": "eng-alert", "includeDescription": false}'
On my test the first call returned 82 jobs. The second call, a minute later, returned only two free rows:
[
{ "status": "error", "company": "airbnb", "errorCode": "NO_DATA", "errorMessage": "airbnb: no new jobs since the last run. Not charged." },
{ "status": "error", "company": "notion", "errorCode": "NO_DATA", "errorMessage": "notion: no new jobs since the last run. Not charged." }
]
So you can always tell "nothing new" from "something broke". Put the same input in an Apify schedule (daily, say) and attach a webhook, or call it from cron, n8n, Make or Zapier, and send the status: "ok" rows to Slack, e-mail or a Google Sheet. Because only new jobs are charged, a daily alert across 20 companies usually costs a few cents a month.
What it costs
You pay per job delivered: $0.0015 on Apify's free plan, down to $0.0008 on higher plans, so 1,000 jobs is $0.80 to $1.50. There's no start fee and no proxy cost, because these are public job-board APIs. Unknown companies, invalid links and "no new jobs" rows are free, and you can set a maximum cost per run.
Related
- ATS Jobs Scraper on Apify
- Google Jobs Scraper, for jobs from job boards and company sites across Google's job search, by keyword and city
- YouTube transcripts from Python, curl and n8n, the same one-call pattern for video
- All our Actors: apify.com/scrapemole
Questions, or a company whose board doesn't work? Open an issue on the Actor page or email us and I'll look at it.