ATS Job Listings API $0.99/1K — Lever Job Board, Job Openings avatar

ATS Job Listings API $0.99/1K — Lever Job Board, Job Openings

Pricing

from $0.99 / 1,000 job postings

Go to Apify Store
ATS Job Listings API $0.99/1K — Lever Job Board, Job Openings

ATS Job Listings API $0.99/1K — Lever Job Board, Job Openings

$0.99/1K | 59,185 job listings in one run — where others stop at 5,000. Straight from the careers page: Greenhouse, Workday, Lever, Ashby, Oracle, BrassRing, UKG +13 more. Lever job listings, Ashby job listings, Greenhouse job listings, Workday job listings, employer job listings, recruiting data.

Pricing

from $0.99 / 1,000 job postings

Rating

0.0

(0)

Developer

Snow Leo Data

Snow Leo Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

🚀 ATS Jobs Scraper — jobs from the employer, not from a job board

$0.99 per 1,000 jobs. Straight from 20 applicant tracking systems — Greenhouse, Workday, Lever, Ashby, SmartRecruiters, Oracle Cloud, BrassRing, UKG and twelve more. Greenhouse data and Workday data reach you through different endpoints with different shapes; this actor hides that and returns one row format for all of them — applicant tracking data you can query without knowing which ATS it came from, ATS data behind a single careers API.

59,185 jobs in one runthe closest competitor stops at 5,000
1,045 company boards built inrun it with an empty input and it works
Salary taken from the source fieldnot inferred by a model, not missing
$0.99 / 1,000others in this niche charge $1.30, $2.50 and $4.00

Not a job board. Not a resold database. These are career page jobs: every run reads the same endpoint the company's own careers page reads, at the moment you run it — so a role closed this morning is already gone, and one posted an hour ago is already here.


Who this is for

Job boards and aggregators. Fill a board with company job listings that are live right now, in the employer's own words, and index them straight away — the output can come out as schema.org JobPosting.

Recruiters and sourcers. Watch a list of target companies and see only what opened since yesterday.

Market and salary research. Salary comes from the field the employer filled in, so a pay study is not built on numbers a model guessed at.

Anyone priced out by a 5,000-job cap. One large employer alone can fill a competitor's entire run: Bosch has 4,834 open roles, Walgreens 22,346.


Quick start

Run it with an empty input. No account slugs, no tenant addresses: the actor falls back to a bundled registry of 1,045 verified company boards and returns 1,000 rows, so a trial costs about a dollar.

That sample is spread out on purpose: one board per platform, no employer over 60 rows. A measured trial returns 50 companies across 19 platforms; picking by size gave 996 of 1,000 rows from Walgreens alone.

Point it at your own companies when you know them — a bare name is enough, the platform is detected for you:

{ "companies": ["stripe", "ramp", "notion"], "keywords": ["engineer"] }

Watching a market instead? Add "onlyNew": true and each run returns only the postings that appeared since the last one.

Full field reference is further down under Input.


Which ATS platforms are supported

Twenty, each read through the same interface the employer's own careers page calls — no HTML parsing, no headless browser, no job-board middleman:

SmartRecruiters job listings, JOIN job listings, Workable job listings, Personio job listings, Recruitee job listings, BambooHR job listings, Rippling job listings and Breezy job listings come back in the same 28 fields as Workday careers data — the platform only shows up in source. Greenhouse, Ashby, Lever, Pinpoint, Oracle Cloud, Eightfold, HireHive, Comeet, BrassRing, UltiPro and Jobsoid are read exactly the same way. The endpoint each is read from, and how many of its boards the registry holds, are in Where each platform is read from below.


What you get back, in plain words

Every platform above runs an employer job feed API for its own careers page, and this Actor reads it directly. What comes back is employer job listings — open positions data from the company itself, not a copy that a job board made of it.

That makes it talent acquisition data rather than a search-result page: the title, the location, the employment type, the salary field when the ATS has one, and the whole advert body. Job posting data assembled this way carries the employer's own wording, because there is no aggregator in between to rewrite it.

The awkward cases are read the same way. A Workday careers site is taken through its own CXS endpoint rather than its rendered page, and any row can be emitted as a JobPosting schema document, so a job data pipeline downstream receives structured objects instead of HTML it has to parse again.

What you can do with the file

Every run leaves a job listings dataset you can pull as JSON, CSV or Excel, or read straight from the dataset API. It is open roles data from the employer's own system: job openings data with the advert body, the apply URL and the salary field the ATS published. Used as employer hiring data or company hiring data it shows who is growing and where; used as a recruitment API it fills an internal board without a second source.

Read as a job postings feed it can be served onward unchanged; job description data arrives as plain text and, where the ATS publishes it, as HTML. Lever job listings, Greenhouse job listings and Workday job listings land in that same file with only source telling them apart, so a job postings dataset built from twenty platforms needs no per-platform mapping of your own.

Common questions

How fresh is a row? Fetched when you press Start — nothing cached, so a role closed this morning is already absent.

Do I have to supply a company list? No. An empty input reaches the bundled registry's 1,045 careers page listings; with your own list a bare company name is enough, because the ATS platform data that routes the request is detected for you.

Is this a feed or a download? Both. The dataset behind a run is an employer data API you read by URL, which makes it a jobs data feed and not only an export.

Why employer job postings and not a job board? A board is a copy. These are direct employer listings, read from the system the employer files the role in, so wording, salary and closing date are the employer's own.

What one row looks like

Every row carries the same 28 fields whatever the platform, and every key is always present — no checking whether a field exists.

{
"source": "ashby",
"company": "ramp",
"company_slug": "ramp",
"job_id": "c63ba7d7-5290-4d9b-b002-40b2873b66f6",
"title": "Partner Consultant, Accounting",
"url": "https://jobs.ashbyhq.com/ramp/c63ba7d7-5290-4d9b-b002-40b2873b66f6",
"location": "Remote (US)",
"locations": [
"Remote (US)",
"Remote (Canada)",
"San Francisco, CA",
"New York, NY (HQ)"
],
"city": "",
"country": "",
"latitude": null,
"longitude": null,
"department": "Sales / Solutions Consulting",
"employment_type": "FullTime",
"experience_level": "",
"remote": true,
"hybrid": false,
"has_remote_option": true,
"posted_at": "2026-09-02T17:17:20.841+00:00",
"posted_days_ago": 8,
"salary_raw": "$151K - $231K",
"salary_min": 151000,
"salary_max": 207000,
"salary_currency": "USD",
"salary_period": "year",
"description_text": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams, embedded in the transaction flow of every dollar a business spends. We automate how over $200B in …",
"description_html": "",
"ref_number": ""
}

remote and hybrid are separate booleans; has_remote_option is the looser flag ("remote is one of the choices") that competitors report as remote, kept apart so you decide which one you meant. salary_period says whether the figure is yearly or hourly, locations keeps every city a posting was listed in, and posted_days_ago saves you parsing the date.


Where each platform is read from

Every platform is read through the same interface the employer's own careers page calls. No HTML parsing, no headless browser, no job-board middleman — which is why a posting closed an hour ago is already gone from the result. The row shape does not change with the platform: Greenhouse jobs, Workday jobs and Oracle jobs differ only in what source says.

PlatformAlso known asRead fromBoards in the registry
GreenhouseGreenhouse.io job boardboards-api.greenhouse.io295
AshbyAshbyHQapi.ashbyhq.com245
PersonioPersonio Recruiting<slug>.jobs.personio.de148
LeverLever.coapi.lever.co70
Recruitee<slug>.recruitee.com68
SmartRecruitersapi.smartrecruiters.com46
PinpointPinpoint HQ<slug>.pinpointhq.com44
WorkdayWorkday CXS, wday cxs<co>.wd1.myworkdayjobs.com/wday/cxs36
BambooHR<slug>.bamboohr.com27
BreezyBreezy HR<slug>.breezy.hr16
Ripplingapi.rippling.com11
Oracle CloudOracle HCM Cloud, Oracle Fusion Recruiting, ORC<tenant>/hcmRestApi recruitingCEJobRequisitions10
JOINjoin.comjoin.com10
Workableapply.workable.com6
EightfoldEightfold AI talent platform<slug>.eightfold.ai4
HireHive<slug>.hirehive.com4
Comeetwww.comeet.co careers API2
BrassRingIBM Kenexa BrassRing, Kenexa Talent Suitesjobs.brassring.com1
UltiProUKG Pro Recruiting, UltiProrecruiting.ultipro.com1
Jobsoid<slug>.jobsoid.com1

The board counts are what the shipped registry holds today, not what each platform is capable of: any board of a supported ATS can be passed in by hand whether or not it is in the registry.


How it compares

This actorfantastic-jobs/career-site-job-listing-apijobo.world/ats-jobs-api
Jobs per runno cap — 59,185 measured in one run5,000 hard cap100 per call, 10,000 via paging
Data agelive, fetched when you run it1 h delay, up to 3 h to ingestindexed within 24 h
Salarytaken from the source fieldinferred by an LLM (ai_salary_min_value)from source
Duplicate handlingsame posting across cities merged into one row, locations kept
Only new jobs since last runyesonlyNew
schema.org JobPosting outputyesoutputFormat
Runs with no configurationyes — 1,045 verified boards bundledyou supply every companyyou supply every company
ATS platforms2054150+

Competitor figures read from their public store listings on 2026-09-10. Where they win: raw platform count. If you need an ATS this actor does not support yet, they cover more. Everything else on this table, this actor does better.


Output as schema.org JobPosting

Set outputFormat to jsonld and rows come out as schema.org JobPosting objects — the format Google Jobs and most job-board engines read, so a board can index this output without writing a mapper first.

{
"@context": "https://schema.org/", "@type": "JobPosting",
"title": "Account Manager | Mid-Market",
"hiringOrganization": { "@type": "Organization", "name": "Ramp" },
"jobLocationType": "TELECOMMUTE",
"baseSalary": { "@type": "MonetaryAmount", "currency": "USD",
"value": { "@type": "QuantitativeValue",
"minValue": 211400, "maxValue": 290600, "unitText": "YEAR" } }
}

Fields the employer never published are left out rather than emitted empty: invalid markup is worse than no markup. A salary without a stated period keeps its numbers and omits unitText instead of guessing "per year".


Pay for new jobs, not for the same ones every morning

Turn on onlyNew and the actor remembers what it already returned. A daily monitor over 40 boards then bills for the handful of openings that appeared overnight instead of the same twenty thousand rows every day.

Measured over three consecutive runs on the same two boards:

run 1221 jobs pushed
run 20 pushed, 221 recognised as already seen
run 30 pushed, 221 recognised as already seen

The memory key is source:company:job_id, so two ATS platforms that happen to use the same internal id never cancel each other out. Neither competitor in the table offers this.

The memory lives in a named key-value store, not the run's default one — the default store is created fresh for every run on the platform, so state kept there would silently vanish and every run would re-deliver the same postings.

A posting is remembered only after it actually reaches the dataset, and the memory is checkpointed after every batch. So if a run hits its timeout or the container moves, the next run picks up where it stopped: jobs already delivered are not billed twice, and jobs that never made it are not silently marked as delivered.


You do not need to know the company's ATS

Every competitor here expects you to supply each company yourself — and for Workday, Oracle Cloud and BrassRing that means digging a tenant address out of a careers page.

This actor ships a registry of 1,045 company boards, each verified with a live request at build time, spanning all 20 platforms. Run it with an empty input and it just works:

1,045 boards · 117,987 jobs at build time · all 20 platforms
CompanyATSJobs when verifiedCountry
walgreensBrassRing22,346
BoschGroupSmartRecruiters4,834Hungary
sgsSmartRecruiters4,468Spain
micronWorkday2,916Singapore
spacexGreenhouse2,376
questdiagnosticsOracle Cloud2,085United States
nvidiaWorkday2,000
jabilWorkday2,000
cwWorkday2,000India
brookdaleOracle Cloud1,954United States
salesforceWorkday1,431
honeywellOracle Cloud1,362United States
maerskWorkday1,353
speechifyGreenhouse1,086United States
emersonOracle Cloud1,059India
wppmediaGreenhouse1,032United Kingdom
bmoWorkday1,011United States
deliveryheroSmartRecruiters979
marksandspencerOracle Cloud884United Kingdom
databricksGreenhouse873Japan
continentalSmartRecruiters861Germany
fordOracle Cloud838United States
openaiAshby785Japan
gopuffLever778United States
coupangGreenhouse697
chubbOracle Cloud688United States
ripplingRippling664Israel
stripeGreenhouse618Israel
transperfectRecruitee604United States
anthropicGreenhouse599United Kingdom

These are the 30 largest of 1039 boards. The full list is a drop-down inside the Actor's input — Or pick companies from the registry — so you can browse every one of them before you run anything.

Measured, zero configuration, top 100 boards: 20,000 jobs in 215 s, 100 of 100 companies succeeded, 0 failures — the run stopped at the cap, not at the end of the registry.

Running it with an empty input is a trial, not an invoice: without companies or a limit the actor takes the 25 largest boards and stops at 1,000 rows, so an accidental click costs about a dollar rather than forty. Ask for the whole registry explicitly with useRegistry: true and maxItems: 0.

Narrow it with registryAts (only Oracle, say), registryCountry, registryMinJobs or registryLimit — or ignore the registry entirely and pass your own companies.

Six entries in the registry are vendor demo boards carrying invented postings — Jobsoid's demo lists a role called rf3, Breezy's lists Employee #12. They prove the platform is readable, so they stay in the registry, but they are kept out of the trial: a sample is a promise about data quality, and fake rows break that promise.

The registry is not a frozen blob, and it does not pretend to be fresh forever. Every run reports when it was built and how old it is; past 120 days the log says so plainly, because a board that changed ATS should not be mistaken for a company that stopped hiring.

tools/build_registry.py rebuilds it: --check re-verifies every board and drops the ones that stopped answering, --add names.txt scans new companies, --countries recomputes countries. Last full verification: 1045 of 1045 boards answered, 0 dropped.

Country came from the boards themselves, not from a guess: 413 of the 1,045 publish one. DE, Germany and Deutschland all select the same 91 boards. The remaining 632 are left out of a country-filtered run rather than assigned a country they never stated.

United States 116 · Germany 91 · United Kingdom 53 · India 21 · France 21 · Spain 11 · Israel 11 · Canada 11 · Poland 8 · Ireland 7 · Netherlands 6

Questions about specific platforms

How do I scrape a Workday careers site?

Give the actor the company slug and it brute-forces the tenant: Workday boards live on myworkdayjobs.com behind a tenant and a site name, and the actor tries the plausible combinations against the Workday CXS endpoint until one answers with postings. That search is the reason you do not have to dig a tenant address out of a careers-page link yourself.

Which Workday data centre is a board on — wd1, wd3 or wd5?

The prefix in the middle of a Workday address is the data centre the tenant was provisioned in, and it differs per company: one board is at <co>.wd1.myworkdayjobs.com, the next at wd3.myworkdayjobs.com, another at wd5.myworkdayjobs.com. The detector walks wd1, wd2, wd3, wd5, wd10, wd12 and wd103 against the cxs jobs endpoint and keeps the one that answers, so you never have to know which myworkdayjobs wd1 or wd3 host a company happens to sit on. The 36 Workday boards in the registry are spread across wd1, wd3, wd5, wd12, wd103, wd108 and wd501, which is why guessing a single prefix does not work.

How do I scrape Oracle HCM Cloud, formerly Oracle Fusion Recruiting?

Oracle HCM jobs are served by the recruiting module of Oracle Fusion — Oracle Recruiting Cloud, ORC for short — from each tenant's own Fusion host. The actor reads the recruitingCEJobRequisitions resource of that tenant's hcmRestApi, which needs no key: give it the host and the site number (almost always CX_1) and it pages through the requisition list. Ten Oracle Cloud boards ship in the registry as working examples.

How do I scrape an IBM Kenexa BrassRing board?

BrassRing boards sit on sjobs.brassring.com and are addressed by a partner id and a site id, both visible in the careers-page link. Supply the pair and the actor reads the board directly. One BrassRing board — Walgreens, with 22,346 postings when it was verified — is in the registry already as a working example.

How do I scrape UKG Pro Recruiting, formerly UltiPro?

UKG boards live on recruiting.ultipro.com and are addressed by a tenant code such as PEN1024PMCO, which appears in the careers-page link. Give the actor that code and the board GUID behind it is resolved automatically, so the only thing you have to find is the part a human can actually see. UltiPro calls its postings opportunities and links each one as an OpportunityDetail address; both the search results and those links are read for you.

How do I scrape a Breezy HR board?

Breezy HR boards are at <slug>.breezy.hr, where the slug is the company's own name in the URL of its careers page. Pass the slug and the board is read through Breezy's public feed. The sixteen Breezy boards in the registry were verified against small boards, so treat very large ones as unmeasured rather than proven.


Why "no cap" matters

A single large employer can exceed a competitor's entire per-run limit. Bosch alone returns 4,834 open roles; NVIDIA 2,000; Honeywell 1,362. A 5,000-job ceiling means one big employer fills your entire run.

Measured, 80 registry boards, no limit set:

59,185 jobs · 35 s · 80 of 80 companies · 0 failures

That is nearly 12× the hard cap of fantastic-jobs, in a single run that finished in under a minute.

Verified on real Fortune 500 boards, not demo tenants: Honeywell, Emerson, Ford and Chubb together return 3,947 live roles through Oracle Cloud Recruiting.

Measured, one run, thirteen companies, no filters:

smartrecruiters 4821 eightfold 590 workable 17
workday 2000 brassring 514 recruitee 12
oracle 1357 rippling 347 ultipro 6
greenhouse 614 ashby 146 pinpoint 5
lever 75 jobsoid 4
join 48 breezy 3
personio 38 bamboohr 1
hirehive 1
------------------------------------------------------------
10,599 jobs · 41.1 s · 19 platforms · 0 failed · 338 duplicates merged

Field coverage — measured, not promised

Percentage of rows where the field is non-empty, from one live board per platform, taken on 2026-09-10. A dash means the employer did not publish it, not that the actor dropped it. Nothing here is guessed or filled by a model.

PlatformBoardJobstitleurllocationcountrydeptpostedtypedescriptionsalary
ashbyramp14610010010010010010010096
bamboohrapify1100100100100100100100
brassringwalgreens6010010073100100
breezybreezy3100100100100100100100100
eightfoldbayer6010010010010010010022
greenhousestripe614100100100100100100
hirehivehirehive1100100100100100100100
jobsoiddemo41001001001005010075
joincarhartt-wip48100100100100100100100
levermatchgroup7510010010099100100979755
oraclehoneywell6010010010010010077
personiourbansportsclub3810010087100100100
pinpointeasygenerator5100100100100100100
recruiteechannable1210010010010010010010010075
ripplingrippling664100100100100100100100
smartrecruitersBoschGroup200100100100100100100100
ultipropenske610010010010010067100
workablerebellion38100100100100100100100100
workdaynvidia60100100100100100100
comeetcomm-it1481001001009010010072100

Title and link are 100% on every platform — those are the two fields a job row is worthless without.

Salary is bold where the employer publishes it as a number. It is taken from the source field, never inferred: fantastic-jobs fills this column with an LLM guess (ai_salary_min_value), and memo23 has no salary field at all.

Remote means remote

Ashby's isRemote flag is set when any listed workplace option is remote — at Ramp it was true for 135 of 146 jobs, while workplaceType said "Hybrid" and the location said "New York, NY (HQ)". Passing that flag straight through would make a "remote only" filter wrong 92% of the time.

This actor reads workplaceType instead. Ramp: 17 genuinely remote, 118 hybrid. The same rule is applied to Lever, Eightfold, Oracle and Rippling.


Duplicates are merged, not dropped

Workable returns one posting once per city. Rebellion's 38 rows are 17 real jobs. Rippling's 664 are 347. Rows sharing an id are merged into one, and every city is kept in locations — you lose nothing and count nothing twice.


Input

{
"companies": [
{ "slug": "stripe" },
{ "slug": "ramp" },
{ "ats": "workday", "slug": "nvidia", "wd": "wd5", "site": "nvidiaExternalCareerSite" }
],
"keywords": ["engineer", "backend"],
"remoteOnly": true,
"postedWithinDays": 14,
"salaryMin": 120000,
"includeDescription": true
}

Leave ats out and the actor finds the platform itself — it probes the platforms in parallel and remembers the answer for a week in the key-value store, not in the container, so a scheduled run does not pay for the same lookup every morning. Measured on three companies: 3.1 s the first time, 0.7 s after.

salaryMin compares annual figures. An $80/hour posting is treated as $166,400/year, not as "80".


Speed — measured

CompanyPlatformJobsTime
WalgreensBrassRing22,346 available25 s / 300
HoneywellOracle Cloud1,3627.3 s
EmersonOracle Cloud1,0598.6 s
FordOracle Cloud83810.1 s
ChubbOracle Cloud6885.4 s
stripeGreenhouse6180.8 s
BoschGroupSmartRecruiters4,8342.7 s
bayerEightfold5884.8 s
nvidiaWorkday2,00017 s
Oracle tenantOracle Cloud2,22018 s
ripplingRippling66419 s

Workday caps at 20 records per request and Eightfold at 10, so those are paged in parallel. Everything else arrives in one or two calls.

Companies are fetched in parallel too, not one after another — the same 30 boards took 444 s serially and 27 s in parallel, a 16.6× difference. On Apify you pay for compute time, so this is money, not just comfort. Each platform is capped at 4 concurrent requests: a hundred Greenhouse boards are one host, and hammering it would earn a 429 rather than more data.


Rate limits are handled, and it is measured

On Apify your requests leave from addresses shared with every other user of the platform, so an ATS can start returning 429 for reasons that have nothing to do with you. Waiting out Retry-After in one thread is not enough — the other threads keep hammering the same host and the failures avalanche.

This actor keeps the back-off per host, visible to every thread at once: one 429 slows the whole host down, the pause doubles on each further 429, respects Retry-After, and decays again as requests start succeeding.

Measured against a local server that accepts 3 concurrent requests and rejects the rest, hit by 20 threads:

Jobs retrieved429s received
With the shared back-off20 of 2027–48
Without it1–3 of 20117–119

The test asserts the success rate, not the number of 429s. Across repeated runs the success rate is steady — 20 of 20 every time, against 1 to 3 without the back-off — while the rejection count swings with whatever else the machine is doing. A threshold on the noisy number makes a suite flaky, not strict.

Hosts that asked us to slow down are listed in the run report, so a slow run is explained rather than mysterious.


Tested

tests/test_logic.py — 10 checks on the rules that were once wrong and got fixed: remote semantics, salary periods, duplicate merging, the salary filter, HTML entities, country normalisation.

tests/test_live.py — one real board per platform, no mocks. It checks volume, that title and url are never empty, and that no duplicate ids survive merging. 20 of 20 platforms green, whole suite in about 30 s.

tools/verify_readme.py — checks this page and the input schema against the code: every field read, every platform listed, no promise the code does not keep. With --live it re-fetches every board in the coverage table to confirm the numbers still hold. Twice during development a figure here drifted from reality and it was luck that caught it, so checking became a script rather than a habit.

The image is built and run before every release, not just imported: the SDK's own dependency tree drifted twice into a state where import apify raised before a line of this code executed, so requirements.txt pins the two transitive packages that broke and says why.

The suites run clean on Python 3.8 through 3.13, deprecation warnings treated as errors — the actor image tracks a newer Python than this code was written for, and a call removed upstream would fail in the cloud, not on a laptop.

tests/run_all.py runs everything at once; --live adds the suites that talk to real employer endpoints, which are kept opt-in because hammering someone else's API on every edit is not a neighbourly habit.

tests/test_resume.py — the whole actor lifecycle against a stand-in for the Apify SDK: a run that dies halfway, the run that resumes it, and a third that should bill nothing. It asserts the memory of delivered jobs matches what actually reached the dataset — the check that caught postings being marked as delivered before they were sent.

tests/test_throttle.py — a real HTTP server on localhost that rate-limits, hit through the real client by 20 threads. Provoking 429s against someone else's production API is exactly the behaviour this code exists to prevent, so the server under test is ours.


Memory, and why the default is plain text

Measured on a 15,000-job run over 60 boards with descriptions on:

Description formatPeak memory
text (default)688 MB
both1,095 MB

The two description fields carry nearly the same content and together account for 98% of a row — everything else, all 25 remaining fields, is the other 2%. Keeping one of them is the difference between fitting in a 1 GB actor and being killed by the OOM reaper mid-run.

Pushes to the dataset are split by payload size, not row count: a posting with a description weighs about 6 KB against 300 bytes without one, so a fixed row count would either waste requests or overshoot the platform's ~9 MB limit. Measured on Stripe's 613 postings with both description fields: two pushes of 4.00 MB and 2.82 MB.

Results are also pushed to the dataset batch by batch, not all at the end, so memory no longer grows with the size of the run — and the first jobs land in the dataset while later companies are still being fetched. The same 15,000-job run peaks at 494 MB streamed, against 688 MB when accumulated.

Suggested memory: 1 GB, and that is not a guess — the heaviest case the actor supports was run inside a container capped at exactly 1 GB: 40 registry boards, descriptionFormat: both, 15,000 rows delivered, exit code 0, no OOM. Without descriptions, 30 boards and 15,000 jobs peak at 368 MB.


A broken company does not break the run

Rows that arrive with neither a title nor a link are dropped rather than delivered — Workday occasionally returns a record holding nothing but an id, and on a pay-per-result plan you would otherwise be charged for it. They are counted in the report as dropped_empty_rows, because silently discarding them would hide a broken adapter.

One unreachable company is reported and skipped; the rest still return. A page that dies halfway through a company's pagination keeps the jobs already collected instead of throwing them away — and says so, rather than passing a partial result off as a complete one.

The run report (REPORT in the key-value store) answers both questions a scraper usually leaves open — what was thrown away, and whether what remains can be trusted:

{
"ok": [ { "company": "ramp", "ats": "ashby", "jobs": 146, "seconds": 0.5 } ],
"failed": [],
"filtered_out": { "keywords": 46, "remote": 84, "salary": 0 },
"merged_same_id": 338,
"failed_requests": { "host.example.com": { "count": 1, "last": "HTTP 404" } },
"throttled_hosts": { "api.example.com": 2.0 },
"result": {
"jobs": 983, "with_salary": 182, "with_date": 983,
"remote": 200, "hybrid": 217, "has_remote_option": 240,
"coverage_percent_by_platform": {
"ashby": { "jobs": 146, "fields": { "salary_min": 97, "posted_at": 100 } }
}
}
}

Coverage is reported per platform on purpose: an empty salary column is normal for Workday and would mean a broken adapter for Ashby.


Getting the data into your own system

A run that ends on the Apify run page is half a job. Here is every route out, in the order people actually use them.

Straight download

The run page offers the dataset as JSON, CSV, XML and Excel. For a one-off list — a market map, a sourcing spreadsheet — this is the whole workflow: press Start, wait, press Export.

The API, for anything repeatable

Every run writes to a dataset that the API serves:

https://api.apify.com/v2/datasets/<datasetId>/items?format=csv&clean=true

If you always want the newest run of this Actor, you do not need to track ids at all — this address always answers with the last finished run:

https://api.apify.com/v2/acts/<actorId>/runs/last/dataset/items?format=json

Add your token as a header or as ?token=. clean=true drops rows the run marked empty, and format also accepts csv, xlsx and xml.

No-code pipelines

Apify ships a maintained connector for the tools most buyers already run. Make, n8n, Zapier and Slack each have one, and the webhook integration covers everything else: Apify calls your URL when the run succeeds and hands you the run id and the dataset id in the payload.

In Make and n8n the pattern is always two blocks — run the Actor, then read its dataset — so the scenario stays the same whether it fires on a schedule or from a form someone filled in.

On a schedule, paying only for what is new

Schedules live in the Apify console and take a cron expression. Point one at this Actor with Only jobs not seen before switched on and you get a daily delta rather than the same board re-delivered: the Actor remembers delivered job ids in a named key-value store that survives between runs, and bills only for rows it hands over.

Name the storage if you intend to keep it. Unnamed storages on Apify are temporary by design; a named one stays put, which is exactly what a delta depends on.

Three things people build with this

A sourcing feed. Pick the ATS platforms you care about, set keywords, run it daily with the delta switch on, and push new rows into a Slack channel or an applicant sheet. The row already carries the apply URL, so nobody has to open the careers page to act on it.

A market map. Leave the keywords empty, take the whole registry, and count openings per company, per country and per platform. Because salary comes from the employer's own field rather than from a model's guess, the pay columns are safe to average.

A job board. Every row is ready to republish: title, full description in text or HTML, location, salary range, employment type and the direct apply link. The schema.org JobPosting output mode exists for exactly this, so a listing page can be indexed by Google for Jobs without a transformation step.


A run succeeded but the dataset is empty — is that a failure?

Not necessarily, and the Actor always says which case it is. The run report carries empty_reason and the log carries a warning naming it: nothing-changed (the incremental mode working as asked — everything had already been delivered by an earlier run, so nothing was written and nothing was charged), filtered-out (your filters removed everything that was collected), source-returned-nothing (the search matched nothing at all), or charge-limit-reached (the run's spending limit was already used up). A silent empty dataset is treated as a defect, not as a normal outcome.

Known limits

  • 20 platforms, against 54 and 150+ for the two competitors above. If your target sits on iCIMS, Taleo, Jobvite or Phenom, this actor cannot read it yet.
  • Workday, Oracle Cloud, BrassRing and UKG need their tenant identifiers. BrassRing takes partner_id and site_id, both visible in the careers-page link; UKG takes a tenant code such as PEN1024PMCO — its board GUID is then found automatically. Comeet takes a uid such as 76.008 from its board URL; its token is read off the public board.
  • Comeet publishes no posting date, only the time a posting was last changed, which is what posted_at carries there.
  • Workday and Oracle Cloud need their tenant address. Workday is brute-forced automatically; Oracle's host cannot be guessed and must be supplied.
  • BambooHR, Breezy, HireHive, Pinpoint and Jobsoid were verified against small boards only (1-22 jobs). Their endpoint shape is confirmed; large-board behaviour is not yet measured. The endpoint shape is confirmed; large-board behaviour is not yet measured.
  • Personio publishes no description or country in its public feed.
  • Some Eightfold tenants block programmatic access (Amgen and Eaton return 403 regardless of headers). Bayer and others work; coverage there is partial.
  • Salary appears only where the employer published it. Roughly half of US postings carry none, and nothing here fills that gap by inference.

Where the jobs this Actor cannot see live

This Actor reads company career pages, so a job exists here only if the employer runs one of the 20 supported systems. Two kinds of vacancy never will: those from employers on an unsupported ATS, and those from markets where small employers post to a national board instead of running a careers page at all. Three sibling Actors cover the second case:

Every one of the four stamps each row with a source field naming the board or ATS it came from, so datasets from several of them can be concatenated and still be told apart afterwards. The column sets themselves differ, because each source publishes different things.