Career Site Jobs Scraper: 9 ATS incl. Workday, Personio avatar

Career Site Jobs Scraper: 9 ATS incl. Workday, Personio

Pricing

from $1.08 / 1,000 jobs

Go to Apify Store
Career Site Jobs Scraper: 9 ATS incl. Workday, Personio

Career Site Jobs Scraper: 9 ATS incl. Workday, Personio

Open jobs from a company's own career site (Greenhouse, Lever, Workday, Personio and 5 more ATS) for a company domain; $1.80 per 1,000 jobs. Posted pay, department, remote flag, description. Monitoring mode returns only new jobs plus free closed-job rows. No recruiter names.

Pricing

from $1.08 / 1,000 jobs

Rating

0.0

(0)

Developer

Arcadium Data

Arcadium Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Give it company domains, careers pages or job board links and it returns every open job as one JSON row: title, department, locations, remote flag, employment type, posted pay, dates, link and description. It reads the company's own board on Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, SmartRecruiters and Workday, so one company list gives one output shape. $1.80 per 1,000 jobs; you pay only for rows you get.

Why this actor

  • Keyed by company domain: hand it stripe.com and it finds the job board itself.
  • The European ATS (Personio, Teamtailor, Recruitee) sit next to Greenhouse, Lever and Workday in one schema.
  • Monitoring mode (onlyChanges) returns only new jobs plus a free closed row per vanished job, and keeps the old state if a board suddenly returns nothing.
  • No recruiter names or contact mailboxes; emails and phones in descriptions are masked.

Built by Arcadium Data: public data, dated and agent-ready, never personal data.

Use with AI agents

Returns open jobs from company career sites (9 job systems) for a company domain; it finds the job board by itself. Agents can call it through the Apify MCP server (https://mcp.apify.com, open source as apify/actors-mcp-server) as arcadium/career-site-jobs: the input schema is read from the actor, the result is a dataset of JSON rows.

Input example (minimal):

{
"companies": [
"stripe.com"
],
"maxJobsPerCompany": 20
}

"Remote engineering jobs posted this week at three companies, at most 100 rows":

{ "companies": ["stripe.com", "ashby:openai", "lever:zoox"], "keywords": ["engineer"], "remoteOnly": true, "postedWithinDays": 7, "maxItems": 100 }

What comes back: one job row per open position (charged), free coverage rows (stated total vs delivered) and a free error row for a company that could not be read. In monitoring mode (onlyChanges) it also returns free closed rows. Filter on the rowType field. An input that fails is reported, not hidden (includeErrorRows, on by default): the error row carries rowType: "error", a code (invalid_input, not_found, blocked, unavailable, filter_unsupported or no_match), a message and the input it is about. Retry on blocked and unavailable later; do not retry the other codes with the same input.

Cost of the call above: 20 jobs at $1.80 per 1,000 plus the $0.001 start fee is about $0.04 on the free plan; paid plans pay less. Free rows are never charged.

What you get

FieldWhat it holds
id<system>___<native id>, for example greenhouse___8172508. Stable between runs.
ats, company, companyTokenThe job system, the company name and its board name there.
title, department, teamAs the company publishes them. Empty when not published.
locations, countries, remoteLocation strings as published, ISO country codes where known, and true, false or null for remote.
employmentType, seniorityFull-time, part-time and so on; seniority only where the system has the field.
salarymin, max, currency, period and source. structured means the company published a pay field. text means the range was read out of the description. null when nothing is stated.
postedAt, updatedAtISO dates. Not every system publishes both.
url, applyUrlThe public posting and the apply link.
descriptionPlain text or HTML (your choice), with email addresses and phone numbers removed.
language, requisitionId, scrapedAtWhere known, plus when the row was collected.

Output example:

{
"rowType": "job",
"id": "ashby___34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"ats": "ashby",
"company": "Ramp",
"companyToken": "ramp",
"title": "Security Engineer, Cloud",
"department": "Engineering",
"team": "Backend",
"locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)"],
"countries": ["US", "CA"],
"remote": false,
"employmentType": "Full-time",
"salary": { "min": 211400, "max": 290600, "currency": "USD", "period": "year", "source": "structured" },
"postedAt": "2026-04-07T17:12:35.753Z",
"url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"description": "About Ramp\n..."
}

Besides jobs, the dataset can hold three kinds of free rows:

  • coverage: one per company. How many open jobs the board states, how many you received, which system it is and how it was found (explicit, url, html or guess). A shortfall flag marks companies where fewer jobs came back than the board states.
  • closed: in monitoring mode, a job that was open last run and is gone now (id, title, url, closedAt).
  • error: a company that could not be read, with the reason.

Input

{
"companies": ["stripe.com", "ashby:openai", "https://jobs.lever.co/zoox"],
"includeDescriptions": "text",
"countries": ["US"],
"keywords": ["engineer"]
}
FieldWhat it does
companiesDomains, careers URLs, job board URLs, or system:token. A plain company name (notion) also works; see "Finding the job board".
atsAllowListRestrict to some systems. Empty means all.
includeSmartRecruitersOn by default. SmartRecruiters' robots.txt asks crawlers to stay away from its API. Switch this off if you want to honour that.
includeDescriptionstext (default), html or none. none is the lightest and still returns pay where the board publishes it as a field.
maxDescriptionCharsShorten descriptions. 0 keeps them whole.
maxJobsPerCompanyCap per company, default 1,000.
maxItemsCost cap for the whole run: stops after this many charged jobs across all companies. Empty means no limit.
keywords, excludeKeywordsMatch against title, department and team.
countriesNames or two-letter codes. Jobs without a stated country are matched on their location text.
remoteOnly, postedWithinDaysFilters. Jobs without a publish date are kept by the date filter.
onlyChangesMonitoring mode, see below.
emitBaselineWith monitoring: deliver all jobs on the first run (default) or only record them.
stateKeyNames the saved state. Use a new one when you change filters.
includeErrorRows, includeCoverageRowsFree rows, on by default.

Finding the job board

For a domain or a careers page, the actor looks for a link to a supported job system. If the page has none, it tries the company name against each system. A board only counts when it has at least one open job. Check detection on the coverage row: guess means the match came from the name alone, and a different company with the same name is possible. Pass system:token when you need certainty.

Monitoring: only changes

With onlyChanges on, the actor remembers each company's open jobs in a named key-value store in your account. Every later run delivers only the new jobs, adds a free closed row per vanished job and skips description downloads for jobs it already knows. Schedule it daily and pipe the result to Slack, a sheet or a CRM. You pay a small fee per company checked, so a quiet day is almost free.

If a board suddenly returns no jobs at all, the actor keeps the old state and reports an error instead of marking everything closed.

The saved state covers every job on the board, not only the ones your filters keep. If you stop a run early (or it hits your maximum cost), only the jobs already delivered are remembered, and the rest, including closed rows, come with the next run. When emitBaseline is off, the first run reads the job list only and makes no per-job requests.

Switching from another career-site actor

Inputs written for the most used career-site actors work unchanged; each renamed field is noted once in the log.

Their fieldOurs
startUrls, organizations, domainFiltercompanies
greenhouseBoards, leverCompanies, ashbyOrgs, smartrecruitersCompaniescompanies as greenhouse:<token>, lever:<token>, ...
titleKeyword, titleSearch, keywords (text)keywords
titleExclusionSearchexcludeKeywords
postedAfter, datePostedAfter, timeRange (24h, 7d, 6m)postedWithinDays
includeDescription: false, descriptionTypeincludeDescriptions
maxItemsTotal, limitmaxItems
onlyNewOrUpdated, stateKeyonlyChanges, stateKey

A per-company maxItems from another actor is read as our run-wide cap; set maxJobsPerCompany for a per-company limit.

Use it from an AI assistant

Use the Apify actor "career-site-jobs" to list open roles.
Input: {"companies":["stripe.com","ashby:openai"],"keywords":["data"],"includeDescriptions":"none","maxJobsPerCompany":200}
Each dataset row with rowType "job" is one position. Key fields: company, title, department, locations, remote, salary, postedAt, url.
Run it, then group the roles by department and list the ones that publish a salary range.

Use it from code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("arcadium/career-site-jobs").call(run_input={
"companies": ["stripe.com", "ashby:openai"],
"includeDescriptions": "none",
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
if job["rowType"] == "job":
print(job["company"], job["title"], job["locations"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('arcadium/career-site-jobs').call({
companies: ['stripe.com', 'ashby:openai'],
includeDescriptions: 'none',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.rowType === 'job').length, 'jobs');

Pricing

You pay per job delivered, at the price of your Apify plan. In monitoring mode (onlyChanges) you also pay a small fee per company checked, and jobs you already know are not billed again. Failed companies, coverage rows and closed rows are free.

Apify planPer 1,000 jobsPer 1,000 companies checked
Free$1.80$1.80
Starter (Bronze)$1.80$1.80
Scale (Silver)$1.58$1.58
Business (Gold)$1.08$1.13
Platinum$1.08$1.13
Diamond$1.08$1.13

Plus a one-off $0.001 per GB of memory when a run starts.

Set a maximum cost per run in the Apify run options and the actor stops before it goes over.

Use cases

  • Sourcing: pull every open role of a target list of companies, filtered by country or keyword.
  • Sales intelligence: a company that opens ten sales roles is growing. Monitor target accounts and act on the change.
  • Job boards and newsletters: a daily feed of new roles across hundreds of employers.
  • Pay research: collect the salary ranges companies publish, by role and city.
  • Market analysis: count open roles per department over time.

Limitations

  • Public career pages only. Nothing behind a login.
  • Supported: Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, SmartRecruiters, Workday. A company whose careers site is a custom page or runs on another system returns a "no board found" error row.
  • Workday boards are found from their address (https://company.wd5.myworkdayjobs.com/Site), from workday:company/wd5/Site, or from a careers page that links to the board. A company name alone cannot find a Workday board. Workday lists show only relative dates, so the actor reads one extra page per new job to get the exact posting date and the description; that makes Workday runs slower than the other systems (a few jobs per second). Boards over 2,000 jobs are read in slices (by job category or similar) and merged; jobs are counted once.
  • Pay is returned only when the company states it. Ranges read out of free text carry source: "text" and can be wrong; check them before relying on them.
  • remote is null when the system does not say. Some systems mark a job remote because one of several locations is remote.
  • Posting dates are what the company publishes. Some systems give no update date, some give a creation date instead of a publish date.
  • Large Greenhouse and Ashby boards are delivered by the system in one piece, so maxJobsPerCompany limits the rows you get, not the data read.
  • Very large boards (tens of thousands of jobs, for example big Workday sites) need a longer run timeout than the 1 hour default; raise it in the run options for the first full run. Monitoring runs after that are much faster.
  • In monitoring mode, descriptions are only fetched for new jobs. Closed rows carry id, title and link, not the full old record.

Privacy

Recruiter names, contact mailboxes and similar fields are never collected. Email addresses and phone numbers inside descriptions are replaced by [email] and [phone]. You get the job, not the people behind it.

FAQ

How fresh is the data? It is read when you run the actor, so it matches what the company shows at that moment.

Can I get only new jobs since my last run? Yes, set onlyChanges and keep the same stateKey. The first run delivers everything (or just records the baseline with emitBaseline off). To start over, use a new stateKey.

A company returned an error. Why? Open its careers page. If the jobs live on a system that is not in the list above, or in a custom page, there is no board to read. Try system:token if you know the board name. Errors are not charged.

Why did I get fewer jobs than the board states? Look at the coverage row: stopReason tells you whether your cap, a filter or the budget ended the list. shortfall: true marks a real gap.

Something looks wrong in the output. Who do I tell? Open an issue on the actor page with the run link. We read them all.