Career Site Jobs Feed avatar

Career Site Jobs Feed

Pricing

from $1.50 / 1,000 job results

Go to Apify Store
Career Site Jobs Feed

Career Site Jobs Feed

Get jobs from company career sites on Greenhouse, Lever, Ashby, SmartRecruiters, Workable and more in one clean format. Filters, salary parsing, new/removed job tracking.

Pricing

from $1.50 / 1,000 job results

Rating

0.0

(0)

Developer

Olalekan Johnson

Olalekan Johnson

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

Normalized job listings from company career sites that run on public ATS job-board APIs. One schema for Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio, BambooHR, and Breezy.

The Actor calls the official public, keyless JSON or XML endpoints those boards already expose to their own career pages. It does not log in, scrape private pages, or use a proxy.

Use cases

  • Recruiting research across a named list of companies, with the same fields no matter which ATS hosts the board.
  • A scheduled feed of new and removed jobs (changesOnly) for a watchlist.
  • Lead lists filtered by keyword, location, remote, department, or recency.
  • Salary coverage when the ATS publishes a range, or when the posting text contains one.

Supported boards

ATSHow to address itPublic endpoint the Actor calls
Greenhousegreenhouse:dropbox or gh:dropboxGET https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true
Leverlever:spotifyGET https://api.lever.co/v0/postings/{slug}?mode=json
Ashbyashby:rampGET https://api.ashbyhq.com/posting-api/job-board/{slug}?includeCompensation=true
SmartRecruiterssmartrecruiters:ServiceNow or sr:ServiceNowGET https://api.smartrecruiters.com/v1/companies/{id}/postings and one detail request per returned job
Workableworkable:huggingfaceGET https://apply.workable.com/api/v1/widget/accounts/{slug}?details=true
Recruiteerecruitee:helloprintGET https://{slug}.recruitee.com/api/offers/
Personiopersonio:personioGET https://{slug}.jobs.personio.de/xml
BambooHRbamboohr:flyio or bamboo:flyioGET https://{slug}.bamboohr.com/careers/list and one detail request per returned job
Breezybreezy:remoteGET https://{slug}.breezy.hr/json

A careers-page URL works too. The Actor reads the public page for a known board link (Greenhouse, Lever, Ashby, and the hosts above). If the HTML has no board link, it takes a brand from the hostname and checks the public APIs. https://boards.greenhouse.io/dropbox resolves as Greenhouse token dropbox without a brand probe.

SmartRecruiters identifiers are case-sensitive. An unknown SmartRecruiters identifier returns HTTP 200 and an empty list, so that company is counted as a success with a note in the run summary. The other boards return HTTP 404 for an unknown token, and that company is recorded as a failed input. One bad company does not fail the run.

Input

The input schema lives in .actor/input_schema.json. The Store's daily automated test uses the prefill: greenhouse:dropbox, maxItems 5, descriptions on. That pull is one small board and finishes in a few seconds.

{
"companies": [
"greenhouse:dropbox",
"lever:spotify",
"https://jobs.ashbyhq.com/ramp"
],
"keywords": ["account executive", "engineer"],
"excludeKeywords": ["intern"],
"location": "United States",
"remote": false,
"department": "Sales",
"postedWithinDays": 14,
"changesOnly": false,
"stateStoreName": "ats-jobs-monitor",
"includeDescription": true,
"maxItems": 100
}
FieldWhat it does
companiesRequired. ats:token, a board URL, or a company / careers URL. A string with commas or newlines is accepted as well as an array.
keywordsKeep a job when the title, department, team, location, employment type, or description contains any keyword. Empty means keep every job.
excludeKeywordsDrop a job that contains any of these.
locationCase-insensitive substring against locations.
remoteKeep jobs the ATS marks remote. Hybrid and unknown are dropped.
departmentSubstring against department or team.
postedWithinHoursDrop jobs older than this, and jobs with no posted date.
postedWithinDaysUsed only when hours is empty. One day is 24 hours.
changesOnlyReturn only new and removed jobs since the previous run with the same state store and the same filters.
stateStoreNameNamed key-value store for that snapshot. Default ats-jobs-monitor.
includeDescriptionInclude description text and HTML. Salary is parsed before the text is dropped. SmartRecruiters and BambooHR skip their detail requests when this is off.
maxItemsStop after this many jobs have been written. Default 100. Prefill 5.

Jobs that filters drop, jobs past maxItems, and unchanged jobs in changes-only mode are not written and are not charged.

The first changes-only run treats the current matching jobs as new, up to maxItems. Jobs that do not fit stay unseen and come back as new on the next run. Removed jobs are reported only after they were actually returned. Changing filters starts a new baseline. State is not written when changesOnly is off.

Output

Every item uses the same fields. The dataset schema is .actor/dataset_schema.json, with overview, salary, and changes views. The default key-value store record RUN_SUMMARY is a per-company report (succeeded, failed, seen, returned, and whether the run stopped on maxItems or the charge limit). Dataset items are jobs only. Errors stay in the summary so they are not billed as results.

FieldMeaning
title, company, companySlug, companyWebsiteTitle, display name, board token, and the website URL when you passed one.
ats, jobIdSource and that source's job id.
url, applyUrlPublic posting and application links.
locationsLocation strings from the posting.
remote, workplaceTyperemote is true only for remote jobs. workplaceType is remote, hybrid, or onsite.
department, team, employmentTypeFrom the ATS. Greenhouse's public board payload has no employment type, so that field is null there.
postedAt, updatedAt, firstSeenAtUTC ISO-8601 (Z). firstSeenAt is set in changes-only mode.
descriptionText, descriptionHtmlPlain text and HTML. Null for Breezy, whose public JSON has no description, and null when descriptions are turned off.
salaryMin, salaryMax, salaryCurrency, salaryPeriod, salaryTextParsed pay. Period is hour, day, week, month, or year. Null when the posting has no amount.
changeStatusactive on a normal run. new or removed on a changes-only run.

Salary prefers structured fields (Ashby compensation, Lever salaryRange, Greenhouse metadata, Recruitee, BambooHR, Breezy). Otherwise the first currency range in the description is used. A nearby CAD, AUD, NZD, or SGD code replaces a bare $. Amounts written as billions or millions, and bare years, are ignored. When a posting has several ranges, the first one is kept.

Example

This item is from a local run of greenhouse:dropbox on 23 Oct 2026. The description fields below are shortened. The dataset contains the full text and HTML.

{
"title": "Account Executive",
"company": "Dropbox",
"companySlug": "dropbox",
"companyWebsite": null,
"ats": "greenhouse",
"jobId": "8159652",
"url": "https://jobs.dropbox.com/listing/8159652?gh_jid=8159652",
"applyUrl": "https://jobs.dropbox.com/listing/8159652?gh_jid=8159652",
"locations": ["Remote - US: Select locations", "Canada", "US"],
"remote": true,
"workplaceType": "remote",
"department": "Direct Sales - AMER (Sub Team)",
"team": null,
"employmentType": null,
"postedAt": "2026-09-01T13:20:17Z",
"updatedAt": "2026-09-08T15:46:44Z",
"firstSeenAt": null,
"descriptionText": "Role Description\nAs an Account Executive on the Growth team, you will help Dropbox expand how we bring our Product Suite and emerging AI products to new customers.",
"descriptionHtml": "<h2>Role Description</h2><p>As an Account Executive on the Growth team...</p>",
"salaryMin": 146500,
"salaryMax": 198300,
"salaryCurrency": "USD",
"salaryPeriod": "year",
"salaryText": "$146,500-$198,300",
"changeStatus": "active"
}

The same run also returned a Breezy posting for Remote, "REMOTE REAL ESTATE OPERATIONS ASSISTANT", at $400–$500 per month (salaryMin 400, salaryMax 500, salaryCurrency USD, salaryPeriod month). Breezy's public JSON has no description, so those fields were null. A Personio posting on personio:personio came back as "Staff Software Engineer, Data Platform" in Product and Tech, on-site in Munich and Berlin.

A run that mixed a missing Greenhouse token with Breezy, Personio, Workable, and https://boards.greenhouse.io/dropbox finished with this summary: Returned 12 jobs from 4/5 companies. 1 company failed and was skipped. Stopped because maxItems was reached. Exit code 0.

A second changes-only run against breezy:remote and personio:personio, after the first run had stored them, returned 0 new, 0 removed and wrote no extra dataset items.

Pricing

Pay per event. Prices to paste into the Actor's Publication → Monetization settings are in .actor/pay_per_event.json. Apify does not read that file on push. You set the events in Console.

EventPriceWhen it is charged
apify-actor-start$0.005Once per run, by the platform. The Actor does not call Actor.charge for it.
job-result$0.0015Once per job written to the dataset. $1.50 per 1,000 jobs.

A run that returns 1,000 jobs costs $1.505. A run that returns nothing costs $0.005.

job-result is the primary event. The Actor writes each job with Actor.push_data(item, charged_event_name="job-result"). When the user's max charge is reached, the item is not stored and the run stops. Further companies are skipped. The run still exits 0 and the summary sets chargeLimitReached.

Before publishing:

  1. Choose Pay per event. Leave pay per event + usage off. This Actor's memory stays at 128–512 MB, so platform compute is the smaller part of the bill. Adding usage pricing on top of events lowers the Store quality score.
  2. Create job-result at $0.0015 and mark it as the primary event. Set apify-actor-start to $0.005.
  3. Delete the synthetic event apify-default-dataset-item. The SDK also attempts to charge that event on every push. If you leave it priced, users pay twice for each job. After you delete it, the platform price is $0. You may see a log line about an unknown apify-default-dataset-item event. That line is harmless when the event is gone.
  4. Set the minimum max total charge to about $0.01 (one start plus one job is $0.0065).
  5. Do not add a second custom "actor start" event. apify-actor-start is already charged by the platform. Charging a name that starts with apify- from code raises an error.
  6. Raising the start event above Apify's default does not add free compute time. Memory for this Actor is capped at 512 MB, under the 1 GB step, so the start event is charged once.

Local runs are not on pay-per-event pricing. The log line Ignored attempt to charge for an event is expected there, and the jobs are still saved.

Price changes for an Actor that already has paying users take effect after 14 days and can be made once a month. Until there are paying users, a price change applies on the next publish.

Reliability

  • Each request retries up to 4 times on HTTP 429, 500, 502, 503, 504, and on network errors. Backoff is 0.5 * 2^attempt seconds plus jitter, capped at 15 seconds. A Retry-After header of 0–30 seconds is honored.
  • Calls to the same host are spaced at least 0.25 seconds apart. There is no proxy and no attempt to look like a browser beyond a stated User-Agent: CareerSiteJobs/1.0 (+https://apify.com; public ATS job-board client).
  • A failure while resolving or downloading one company is logged, stored on that company in RUN_SUMMARY, and the run continues.
  • Detail-request failures (SmartRecruiters, BambooHR) keep the job and leave the description empty.
  • The run status message is the same sentence stored on RUN_SUMMARY.message.

FAQ

Can I pass a company website instead of a board token?
Yes. Paste the careers URL or the homepage. A known board URL is the most reliable form. A custom careers site that never links to its ATS, and whose hostname does not match the board's company name, needs an ats:token.

Why did SmartRecruiters return zero jobs with no error?
That API returns HTTP 200 and totalFound: 0 for an identifier it does not know, and also for a real company with no open jobs. The summary note says so. Check the identifier's case against the company's SmartRecruiters URL.

Are removed jobs charged?
Yes, when changes-only mode returns them. They are results. Unchanged jobs are not returned and are not charged.

Does the remote filter include hybrid jobs?
No. Hybrid is remote: false. Jobs with an unknown remote flag are excluded when remote is true.

Where is the monitor state?
In the named key-value store from stateStoreName, one key per board: board-{ats}-{token}. The value stores job ids and a hash of the filters. It does not store descriptions.

What does the daily Store test run?
The input prefill: one Greenhouse board (dropbox) and at most 5 jobs, with descriptions. A local run of that input returned 5 jobs in about a second.

Run locally

Python 3.12 or newer. Docker is only required for apify run / apify push.

python3 -m pip install -r requirements.txt -r requirements-dev.txt
mkdir -p /tmp/career-site-jobs/key_value_stores/default
cat > /tmp/career-site-jobs/key_value_stores/default/INPUT.json << 'EOF'
{
"companies": ["greenhouse:dropbox", "breezy:remote", "personio:personio"],
"maxItems": 10,
"includeDescription": true
}
EOF
APIFY_LOCAL_STORAGE_DIR=/tmp/career-site-jobs python3 -m src.main

Jobs are written to /tmp/career-site-jobs/datasets/default/. The summary is the key-value record RUN_SUMMARY.

With the Apify CLI installed, apify run builds the Dockerfile and reads storage/key_value_stores/default/INPUT.json in the project directory.

python3 -m pytest -m "not integration"
python3 -m pytest tests/test_integration.py

The integration test calls the live public boards (Dropbox, Spotify, Ramp, ServiceNow, Hugging Face, Helloprint, Personio, Fly.io, and Remote). Set ATS_SKIP_LIVE=1 to skip it.

Deploy

The account owner publishes this. Nothing in this repo pushes to Apify or stores a token.

  1. Install the CLI: npm install -g apify-cli, then apify login.
  2. From this directory, apify push.
  3. In Console, leave the permission level at Limited permissions. Memory is already limited in .actor/actor.json to 128–512 MB, default 512 MB.
  4. Set pay-per-event prices as described under Pricing. Delete apify-default-dataset-item.
  5. Run once with the default input and confirm the dataset has jobs and the run finishes well under 5 minutes.
  6. Add a logo and the Store description in Console, then publish.

Sources that are not included

  • Workday. The career-site API is undocumented, uses relative dates, and often returns placeholder locations. It is out of scope.
  • Recruitee's authenticated Careers Site API and BambooHR's authenticated ATS API. Both need a customer token. The Actor uses only the public careers JSON (Recruitee offers feed, BambooHR /careers/list).
  • Breezy job-detail pages. https://{slug}.breezy.hr/json is public JSON and is included. The per-job .json URL returns HTML or a redirect, so descriptions are left null rather than scraped.

Open risks

  • A vendor can change a public payload without notice. Fixture tests lock the current field names. A live integration test covers one real board per ATS.
  • A company can move off an ATS. The old token then 404s and is reported on that company only. Auto-detect can also match the wrong board when a hostname brand is a substring of another company's name. Two confident matches produce an error asking for ats:token.
  • Custom career sites that do not mention their ATS in the HTML depend on the brand probe (up to nine public requests). stripe.com/careers is the case the probe is there for: the page does not contain the string "greenhouse".
  • Greenhouse content=true downloads the whole board even when maxItems is small. Dropbox is a few dozen jobs. A large Greenhouse or Ashby board is a multi-megabyte response. SmartRecruiters and BambooHR then do one detail request per job that will actually be returned, spaced 0.25 seconds apart on that host.
  • The first changes-only run can return, and charge for, the whole matching backlog up to maxItems.
  • Salary text parsing keeps the first range. Zone tables with several bands (the Dropbox example has a second US band) are only partly represented. salaryText still shows the matched phrase.
  • Personio's public XML sometimes has empty jobDescriptions. The job is still returned, with a null description.
  • BambooHR's list often has null locations. Location and description are filled from the detail request when descriptions are on.
  • If apify-default-dataset-item is left enabled in Console, each job is billed twice. See Pricing.
  • Actor.push_data does not send an idempotency key. A resurrected run can charge again for items it pushes a second time. Keys on Actor.charge expire after about 3 minutes anyway.

For maintainers

  • src/main.py is the Actor entry. src/ats_jobs/runner.py is the run loop and is what the unit tests call, with a fake HTTP client and a fake push.
  • One module per ATS under src/ats_jobs/sources/. Each source implements probe, fetch_list, and normalize. needs_detail plus enrich covers SmartRecruiters and BambooHR.
  • Recorded fixtures live in tests/fixtures/. tests/fixtures/personio_description_schema.xml is a schema sample with a description and a salary, because the live Personio XML captured for tests had an empty description. The live XML is personio.xml.
  • Add a source only when it has a public, keyless, documented or obviously public careers endpoint. Follow redirects only when the final host is that company's own board host.