Career Site Job Board Scraper: Greenhouse, Lever, Ashby +4 ATS
Pricing
from $5.20 / 1,000 company summary returneds
Career Site Job Board Scraper: Greenhouse, Lever, Ashby +4 ATS
Live jobs scraper for 7 public ATS and career-site boards: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio and Recruitee. Paste company slugs or careers URLs and get every open role with title, location, remote, department, salary, apply URL and posting age, plus hiring momentum.
Pricing
from $5.20 / 1,000 company summary returneds
Rating
0.0
(0)
Developer
daehwan kim
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
13 hours ago
Last modified
Categories
Share
Career Site Job Board Scraper — Greenhouse, Lever, Ashby & 4 more ATS
Paste a list of companies. Get back every open role on their job board, read live at the moment you run it, with a judged one-line verdict on each posting.
No database of yesterday's jobs. No 5,000-job ceiling. No API key.
in → ["https://boards.greenhouse.io/dropbox", "https://jobs.lever.co/binance"]out → one row per open role + one hiring-momentum summary per company
Why this one
Most ATS scrapers on the store hand you a cached copy of someone else's crawl, capped at a few thousand rows per run, and then point you at a paid API when you need more. This Actor calls the company's own job board at run time, so:
| This Actor | |
|---|---|
| Freshness | Read live, per run. A role posted an hour ago is in your first run. |
| Per-run job limit | None. A 824-posting board comes back whole. |
| Coverage | Seven boards — Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio and Recruitee — auto-detected from a slug or URL. |
| Judgement | Every row is scored, not just copied — see below. |
| Movement | Hand it the dataset of an earlier run and each row says whether it is new, each company says how its open-role count moved and which roles are gone. |
The judgement layer
Other scrapers give you fields. This one gives you a verdict:
headline— the row in one line:Senior Backend Engineer — Remote (United States), posted 3d ago, $180k–220kfreshness—fresh(≤30d),aging(≤90d),stale(older)postedAgeDays— how long this role has actually been openghostJobRisk—none/low/medium/high, with the reasons inghostJobFlags: open over 90 or 180 days, never updated, or a duplicate title sitting on the same boardseniority—intern/junior/senior/principal/manager/director/executive, read from the titlesalary—min,max,currencyparsed out of whatever the board publishedremoteandworkplaceType— normalised across four boards that each say it differentlykeywordHits— which of your keywords this role matched
And once per company, a company-summary row: department and seniority mix, remote share, average posting age, top locations, and a hiring-momentum verdict — hiring hard, steady, slowing or stalled, based on how many roles opened in the last 30 days.
Input
| Field | Type | Description |
|---|---|---|
companies | array | Company slugs or careers-page URLs. All seven boards are detected automatically. |
company | string | Alternative to the list — a single company. |
keywords | array | Keep only roles whose title contains one of these words. Empty = every role. |
locations | array | Keep only roles whose location mentions one of these (Remote works). Empty = everywhere. |
postedWithinDays | integer | Keep only roles first published within N days. 0 = no date filter. |
maxJobsPerCompany | integer | Ceiling per company. 0 = unlimited, which is the default. |
includeDescription | boolean | Add the full job description as plain text. Off by default — descriptions are large. |
monitorMode | boolean | Return only what changed since your previous run (default false). See Monitor mode below. |
monitorKey | string | Optional name for the watch, e.g. competitor-hiring-weekly. Empty = derived from your input. |
includeUnchanged | boolean | Monitor mode only — also return roles that are still open and unchanged (default false). |
previousDatasetIds | array | Dataset IDs of earlier runs, used as the baseline for the change fields. Empty = comparisonStatus: no_baseline. |
disclaimerAck | boolean | Acknowledges how this Actor sources data (see Data sources & responsibility). |
Accepted address formats:
dropbox (bare slug — probed across all seven boards)https://boards.greenhouse.io/dropboxhttps://job-boards.greenhouse.io/stripe/jobs/12345https://jobs.lever.co/binancehttps://jobs.ashbyhq.com/linearhttps://apply.workable.com/bluegroundhttps://jobs.smartrecruiters.com/Boschhttps://teeturtle.recruitee.comhttps://personio.jobs.personio.de
A URL is always faster and unambiguous. A bare slug costs up to seven probe requests.
Example
{"companies": ["https://boards.greenhouse.io/databricks","https://jobs.ashbyhq.com/linear"],"keywords": ["engineer", "designer"],"locations": ["Remote", "United States"],"postedWithinDays": 30,"maxJobsPerCompany": 0,"includeDescription": false,"disclaimerAck": true}
Output
One row per open role (rowType: "job"):
| Field | Description |
|---|---|
headline | The one-line verdict |
company, companySlug, inputTarget | Who this role belongs to, and what you typed |
ats, atsLabel | greenhouse / lever / ashby / workable |
jobTitle | Role title |
location, locations | Primary location and every listed location |
remote, workplaceType | true/false/null, and remote / hybrid / onsite |
department, departments | Team and department names |
employmentType | Full-time, contract, intern, … where the board publishes it |
postedAt, updatedAt | ISO dates from the board |
postedAgeDays, updatedAgeDays | Age in days |
freshness | fresh / aging / stale / unknown |
ghostJobRisk, ghostJobFlags | Risk band and the reasons behind it |
seniority | Inferred from the title |
salary | { min, max, currency, raw } when the posting publishes pay |
keywordHits | Your keywords that matched |
applyUrl | Direct application link |
jobId, requisitionId | Board identifiers |
jobDescription | Plain text, when includeDescription is on |
sourceUrl, scrapedAt | Exactly which endpoint was read, and when |
changeType, changeDetail | Monitor mode only — new / changed / removed / unchanged, and what moved |
One row per company (rowType: "company-summary"): hiringMomentum, hiringMomentumReason, totalJobsOnBoard, jobsMatchingFilters, jobsDelivered, remoteJobs, remoteSharePercent, newLast30DaysPercent, averagePostingAgeDays, jobsWithPublishedSalary, departmentDistribution, seniorityDistribution, topLocations.
Rows with rowType: "notice" are status messages — an address with no board behind it, a company with no open roles, a limit reached. Notice rows are never charged.
Invalid input ends the run as SUCCEEDED with a notice row.
Monitor mode (weekly change alerts)
Hiring is a moving target. Turn on Monitor mode and the Actor compares this run with your previous run and returns only what moved on those boards:
changeType | Meaning |
|---|---|
new | A role that was not on the board the last time you ran this — the one recruiters and job-seekers actually want |
changed | The title or location moved; changeDetail shows what it said before |
removed | No longer listed: filled, withdrawn or expired |
unchanged | Still open, still identical — withheld by default, set includeUnchanged to receive it |
Every run also returns one free change-report row, e.g. 12 new job postings, 3 gone, 1 changed since 2026-08-19, with newCount, removedCount, changedCount and unchangedCount. A quiet week returns just that row and no job rows.
The company summary row still describes the whole board (hiring momentum, department mix, remote share) so the change rows always have context.
Put it on a weekly schedule (3 steps)
- Run the Actor once with Monitor mode on — the first run saves a baseline and returns every open role.
- On the run's page click Actions ▸ Schedule Actor, and pick
Every week. - Add an integration on the schedule (Slack, email, webhook) so new roles at the companies you watch reach you without opening Apify.
Runs are matched by monitor key. Leave monitorKey empty and it is derived from your input, so one schedule keeps one history automatically; set it (e.g. competitor-hiring-weekly) to keep several independent watches. State is kept in a named key-value store holding only job identifiers and a short title/location label — never descriptions.
A board that could not be read this run is left untouched: its roles are never reported as "removed" because of a network error.
Compare against an earlier run
Monitor mode keeps its own history. previousDatasetIds does the opposite: you choose the baseline, by pasting the dataset ID of any earlier run of this Actor. Nothing is stored between runs and nothing extra is charged — the comparison arrives as extra fields on rows you were already getting.
On every job row:
| Field | Meaning |
|---|---|
comparisonStatus | compared, compared_without_closures, baseline_unreadable, or no_baseline when you gave none |
isNewSinceBaseline | The role was not in the baseline. null without a baseline. |
previouslySeenAt | When the baseline last saw this role |
postedAtChangedSinceBaseline | The board restamped the posting date — often a repost |
On every company-summary row:
| Field | Meaning |
|---|---|
rolesOpenNow / rolesOpenInBaseline / rolesOpenChange | Open roles then, now, and the difference |
newRolesSinceBaseline | How many of today's roles are new |
closedSinceBaseline / closedRoles | How many are gone, and the first 25 of them with title, location and last-seen date |
hiringChangeHeadline | The same thing in one line |
Two rules keep this honest. Without a baseline there is no trend: comparisonStatus is no_baseline and every change field is null — today's role count is never dressed up as movement. And closures are only claimed when the run read whole boards: if keywords, locations, postedWithinDays or maxJobsPerCompany were set, a role missing from this run may simply have been filtered out, so the status is compared_without_closures and the closure fields stay null.
Pricing
Pay per event:
| Event | Price | When |
|---|---|---|
run-started | $0.005 | Once per run, when your company list is accepted and the boards are read. |
job-listed | $0.003 | Once per job posting written to the dataset, with its full judged row. |
company-summary | $0.01 | Once per company, for the summary row. Only written when at least one job row was delivered for that company. |
A 40-role company therefore costs 0.005 + 40 × 0.003 + 0.01 = $0.135.
Notice rows — unreadable board, no open roles, no matching filters, limit reached, and the monitor change report — are not charged.
In Monitor mode the same job-listed price applies to each delivered change row, removed rows included: a role that has just been filled is exactly the answer you scheduled the run for. A week with no movement delivers no job rows and therefore costs only the run-start fee.
Data sources & responsibility
This Actor reads the public, unauthenticated job-board endpoints that each applicant tracking system serves to a company's own careers page:
Greenhouse https://boards-api.greenhouse.io/v1/boards/<slug>/jobs?content=trueLever https://api.lever.co/v0/postings/<slug>?mode=jsonAshby https://api.ashbyhq.com/posting-api/job-board/<slug>?includeCompensation=trueWorkable https://apply.workable.com/api/v1/widget/accounts/<slug>?details=trueSmartRecruiters https://api.smartrecruiters.com/v1/companies/<slug>/postings?limit=100Recruitee https://<slug>.recruitee.com/api/offers/Personio https://<slug>.jobs.personio.de/xml
- One request per company on five of the seven boards. SmartRecruiters pages its feed at 100 postings, so a larger board costs one request per page. SmartRecruiters and Personio omit the job description from the list feed, so turning on
includeDescriptionadds one read of the public posting per role on those two boards only — leave it off and they cost one request like the rest. - Two companies at a time, with an honest bot User-Agent (
ntriq-ats-jobs/1.0). - No login, no captcha, no anti-bot measure is bypassed. Nothing behind an authentication wall is read.
- Only job-posting data is returned. No recruiter names, no applicant data, no personal contact details.
This Actor is unofficial and is not affiliated with, endorsed by, or sponsored by Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio, Recruitee, or any employer whose board you read. Those names appear only to say which public boards it reads. You are responsible for complying with the terms of service of each applicant tracking system and each employer, and for using the results lawfully. You are the data controller for everything you collect with it, including any obligations under GDPR or comparable law.
ghostJobRisk is a heuristic built from posting age, update dates and duplicate titles. It is a signal to investigate, not a statement about any employer's hiring intentions.
Notes
- Boards that publish no posting date are still returned; their
freshnessisunknownandpostedWithinDaysnever filters them out. - Ashby and Lever publish pay ranges more often than Greenhouse and Workable, so
salaryis populated unevenly. It is parsed from what the board actually published — nothing is estimated. - A company whose board exists but has no open roles returns a free
no-jobsnotice, not an error.
Works well with
- whois-domain-lookup — Qualify the hiring company by its domain: registrar, age and registrant details.
- lighthouse-website-audit — Score the company site for performance and SEO before you pitch or apply.
- theharvester-osint — Collect public emails and hosts for the same company domain.
Every run of this Actor also returns these suggestions in the nextSteps field of its summary row.