Company Jobs Scraper: Greenhouse, Lever & Ashby
Under maintenancePricing
from $1.00 / 1,000 job results
Company Jobs Scraper: Greenhouse, Lever & Ashby
Under maintenanceTrack jobs from company domains or ATS boards across six providers. Filter roles, return one JSON schema, and schedule new or changed jobs. No login or proxy.
Pricing
from $1.00 / 1,000 job results
Rating
0.0
(0)
Developer
rovnaki
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 days ago
Last modified
Categories
Share
Give it the companies you follow and get their open jobs in one table. Reads six public ATS feeds, with title/location filters and an optional mode that returns only new or changed jobs. Start with the explicit board example below; domains and career URLs are also accepted.
This company jobs scraper reads public ATS APIs for Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Recruitee. Use it as a company careers API or incremental jobs feed for job monitoring. No login or proxy is needed; there is no official affiliation with the ATS providers.
Quick start
{"companies": ["greenhouse:stripe","lever:spotify","ashby:ashby"],"maxJobsPerCompany": 5,"maxChargedResults": 15,"includeDescription": false,"onlyNewSinceLastRun": false,"concurrency": 2}
Up to 15 delivered jobs and three fetched boards cost $0.021 in custom events, plus start: $0.0212 at 4 GB or $0.02105 at ≤1 GB. A zero-match board still incurs a company event. Inspect the default key-value store's OUTPUT record for company errors, counts and stop reasons.
Illustrative compact output row (the dataset includes additional nullable fields):
{"id":"ashby:ashby:example-job","company":"ashby","ats":"ashby","title":"Engineer","location":"Remote","remote":true,"change_type":"new"}
Export JSON, CSV or Excel from the dataset. Use it as a Greenhouse jobs scraper or a Lever jobs API, or to monitor Ashby jobs. It reads the boards you supply; it does not discover a global jobs database or produce closed-job rows.
Daily watchlist
Schedule this capped input daily, with one run at a time:
{"companies": ["greenhouse:stripe","lever:spotify","ashby:ashby"],"maxJobsPerCompany": 5,"maxChargedResults": 15,"includeDescription": false,"onlyNewSinceLastRun": true,"concurrency": 2,"filters": {"keywords": ["engineer"],"remoteOnly": true}}
Keep the company list, filters and description setting stable to reuse the same state scope. The first run emits a baseline; later runs omit unchanged jobs. Zero matches still cost company events, and no closed-job rows are produced. Inspect OUTPUT if a company fails. See incremental monitoring below for state and concurrency limitations.
Inputs and discovery
You can also use stripe.com, https://www.spotify.com/careers/, https://jobs.ashbyhq.com/ashby, smartrecruiters:BoschGroup, workable:skroutz, or recruitee:bunq. Domain discovery reads HTML, follows up to two careers links, recognizes ATS URLs/embeds and redirects, then probes up to two domain-derived slugs on the six providers. Explicit ats:slug inputs avoid discovery ambiguity. Keep slug casing, particularly for SmartRecruiters.
| Input | Behavior |
|---|---|
companies | Required array of 1–1,000 domains, URLs or ats:slug strings. Duplicate strings and duplicate resolved boards are processed once. |
filters.keywords | Case-insensitive title substring matches, for example ["engineer", "developer"]. |
filters.locations | Substring match against any published location label, for example ["Singapore", "London"]. |
filters.departments | Substring match against the published department. Does not substitute team labels. |
filters.remoteOnly | Include only roles explicitly marked remote or with a remote location label. Hybrid and unknown roles are excluded. |
filters.postedWithinDays | Positive integer: include postings published within the last N days. Excludes unknown publication dates. |
includeDescription | Default false. Add description_text and description_html. SmartRecruiters uses extra detail requests when enabled. Workable detail requests are always used for salary and accurate metadata. |
onlyNewSinceLastRun | Default false. When enabled, emit new or updated jobs and skip unchanged jobs. |
maxJobsPerCompany | Default 1,000; Store prefill 5. Maximum emitted rows per company after filters and change detection, not a limit on fetched jobs. |
maxChargedResults | Optional run-wide job result cap. Zero skips processing. Company events are priced separately. |
concurrency | Default 2, maximum 4. Per-host request starts are spaced by at least 200 ms; publication and charging are serialized. |
All filter groups must match. Values within a list are alternatives (OR). Omit a filter or use an empty list to disable it. Keyword matching uses titles regardless of includeDescription.
Output example
Illustrative row from the recorded Ashby response; first_seen_at depends on when your input scope first delivers the job:
{"id": "ashby:ashby:7458d4e9-da2e-47bd-98cb-adfda43d42b2","company": "ashby","ats": "ashby","title": "Engineering Manager - EU","department": "Engineering","team": "EMEA Engineering","location": "Remote - European Union","locations": ["Remote - European Union", "Spain", "Italy", "Germany"],"remote": true,"employment_type": "FullTime","salary": { "min": 110000, "max": 185000, "currency": "EUR", "interval": "1 YEAR", "text": "€110K - €185K" },"posted_at": "2024-03-04T14:29:08.532Z","updated_at": null,"url": "https://jobs.ashbyhq.com/ashby/7458d4e9-da2e-47bd-98cb-adfda43d42b2","apply_url": "https://jobs.ashbyhq.com/ashby/7458d4e9-da2e-47bd-98cb-adfda43d42b2/application","first_seen_at": "2026-10-15T00:00:00.000Z","change_type": "new"}
The example abbreviates the locations array. Actual results preserve every distinct location supplied by that posting. Composite IDs include the ATS and board slug, avoiding collisions. Company names fall back to the board slug when the feed has no company name. Missing metadata is null; no salary, date, team or remote status is invented. Employment labels retain each provider's vocabulary. Salary uses the first published salary tier/range, with currency and interval when available; text summaries are retained if structured values are absent. Other compensation such as equity is not converted to salary.
The dataset has jobs and details views. Company errors are saved separately in the default key-value store's OUTPUT record, linked from the Output tab. The summary contains each input's resolved provider, status, fetched/matched/pushed counts, errors, event counts, and any stop reason. A failed company does not crash other companies or add an error row to the jobs dataset. Check OUTPUT.status; a run can finish normally with partial, failed, or limited status.
Incremental monitoring
The named key-value store ats-jobs-feed-state persists across runs. Keys contain a SHA-256 hash of the sorted company inputs, normalized filters and description setting, followed by a board hash. The comparison stores a content hash and original first_seen_at for successfully delivered jobs.
The first run emits matching jobs as new. Later runs classify normalized changes as updated and identical rows as unchanged. With onlyNewSinceLastRun=true, unchanged rows are omitted and incur no job-result fee. Company processing still has a fee. Changing the company list, filters or description setting starts a separate baseline. Toggling incremental mode or changing limits/concurrency keeps the baseline.
Unemitted jobs are not marked seen. A capped incremental run can deliver the next unseen jobs on its next run. New and updated classifications compare the last successfully delivered version, including source update timestamps. Removing a job from a board does not emit a closed record; the schema reports open jobs with new, updated, or unchanged only. Missing jobs remain in state so a reappearing identical posting is unchanged. Delete the named store to reset all baselines.
Use one scheduled run at a time per input scope. The named store is not a distributed lock: concurrent runs of the same scope can emit duplicates or overwrite each other's state. State is checkpointed after each company, including partial delivery on a budget stop. A process termination or storage/billing failure between delivery and checkpoint can cause a duplicate on retry; delivery, billing and state are not one transaction.
Pay-per-event pricing
| Event | USD event price | Charged when |
|---|---|---|
apify-actor-start | $0.00005/GB, minimum 1 GB | Default platform event, automatically at run start. |
company-processed | $0.002 | One board resolves and its listing is fetched and validated, even with no matching jobs. Duplicate resolved boards are skipped. |
job-result | $0.001 | One normalized job is actually written to the dataset. |
The prefilled run delivers up to 15 jobs across 3 fetched boards: up to $0.021 in custom events, plus the memory-based start event. At 4 GB that is $0.0212; at 512 MB (the local default) it is $0.02105. The start event costs $0.00005 per GB with a one-GB minimum. Check the effective run memory and Store pricing at execution time, including any separately applicable platform usage.
Event definitions and suggested prices are in .actor/pay_per_event.json. This is a publishing configuration reference; the CLI does not automatically activate it from actor.json. For a future publishing pass, verify that Console matches these events, retain the default start event and keep the synthetic apify-default-dataset-item fee disabled. Check Console for the applicable platform-cost allocation; it is not established by this source manifest. The code fails closed on missing PPE events or a positive synthetic dataset fee to prevent incorrect billing.
The Actor calls Actor.charge() once per company and once after each successful job push. It checks the SDK's event allowance and current prices before charging. It also respects ACTOR_MAX_TOTAL_CHARGE_USD, maxChargedResults, and the result-cap environment variables ACTOR_MAX_PAID_DATASET_ITEMS, APIFY_MAX_PAID_DATASET_ITEMS, and ACTOR_MAX_CHARGED_RESULTS, using the tightest limit. Existing platform event counts, including the start event, are included in budget checks. Writes and charges are serialized so concurrent company fetches cannot spend the same remaining budget. Exhaustion stops gracefully and records the reason in OUTPUT.
Local runs do not bill an account. Supplied local limits are enforced using suggested company/job prices; Apify's synthetic start event is not charged locally.
FAQ
Do I need an ATS API key, login, or proxy? No. Only public job-board APIs are used. Private postings and authenticated recruiting APIs are excluded.
Why does a company domain fail to resolve? Board slugs can differ from company domains. Careers pages may render links only with JavaScript, use unsupported ATS providers, or link multiple unrelated boards. Supply the ATS board URL or ats:slug to remove ambiguity. Generic empty probe responses are not treated as proof of a company's board; explicit board inputs support zero jobs.
Is this a global jobs search engine? No. It reads the company boards you provide. It does not crawl LinkedIn, Indeed, or aggregators, and does not resolve subsidiary relationships or search multiple boards for a single company. Discovery chooses the first valid board it finds.
Can I use descriptions on my website? The Actor returns source HTML as data. Sanitize it before rendering and follow the job publisher's terms for reuse. It does not collect applications or candidate records.
How does it handle errors? Requests have 15-second timeouts, up to two retries for network errors, HTTP 429 and 5xx responses, exponential backoff with jitter, and a bounded Retry-After delay. Responses are limited to 40 MB and redirects to five hops. Company failures are isolated and summarized in OUTPUT. Pagination repeats and malformed responses fail visibly instead of silently returning an incomplete board.
Limitations
- Only the six listed ATS providers and their public listed jobs are supported. Workday, Personio, BambooHR, Teamtailor and authenticated or regional Lever/Greenhouse endpoints are not implemented.
- No browser is used. JavaScript-only discovery and unusual custom embeds may require explicit board slugs. Slug probing is a heuristic, not proof that a domain owns a board.
- Provider metadata varies. Remote unknowns are
null; hybrid roles are not remote-only matches. Compound location labels remain intact. Dates can be absent or reflect publication/republication; Lever exposes creation time. Time filters exclude unknown dates. - A source may place salary only in description prose. Structured extraction covers supported salary fields and Ashby/Lever salary summaries; it does not parse arbitrary compensation prose. Multiple salary ranges are reduced to the first salary range/tier.
- The list APIs can download all jobs even when emission limits are small. SmartRecruiters and Lever pagination have a 100,000-job safety bound. Extremely large feeds, rate limiting and detail requests increase run time and cost.
- Live daily smoke coverage uses the three prefills. Workable, Recruitee and SmartRecruiters have recorded-fixture coverage and were verified live during development, but are not in the default daily smoke input.
- Scheduled batch execution is supported; a persistent Standby HTTP server and closed-job notifications are not implemented.
Supported public sources
These populated endpoints returned HTTP 200 without authentication using curl on 2026-10-15:
| ATS | Verified company | Official public endpoint |
|---|---|---|
| Greenhouse | Stripe | https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true&pay_transparency=true |
| Lever | Spotify | https://api.lever.co/v0/postings/spotify?mode=json&limit=2 |
| Ashby | Ashby | https://api.ashbyhq.com/posting-api/job-board/ashby?includeCompensation=true |
| SmartRecruiters | Bosch Group | https://api.smartrecruiters.com/v1/companies/BoschGroup/postings?limit=2 |
| Workable | Skroutz | https://apply.workable.com/api/v1/widget/accounts/skroutz |
| Recruitee | bunq | https://bunq.recruitee.com/api/offers/ |
SmartRecruiters job details at /v1/companies/BoschGroup/postings/744000155667010 and Workable details at /api/v2/accounts/skroutz/jobs/DB1A13AC0A were also verified without authentication. Recorded responses are in tests/fixtures; each list fixture keeps its first two jobs.
Local development and health check
cd /home/ubuntu/projects/sideIncome/apify/ats-jobs-feedsource ~/.nvm/nvm.shnvm use 22npm cinpm run buildnpm testnpm run smoke
npm run smoke builds the Actor, launches node dist/main.js in an isolated local Apify storage directory using INPUT.json, and validates every row against the dataset JSON Schema. It requires each prefilled company to return jobs, checks summary success and duplicate IDs, prints a single JSON result line after npm's script banner, and exits non-zero on failure. It strips inherited Apify platform credentials, pricing and limits from the child process so a health check runs locally without billing. Child logs are captured and included in the failure summary. Storage is retained under .smoke-storage/<run-id>/ for diagnosis. For a JSON-only cron output, use npm --silent run smoke.
To run arbitrary local inputs, set APIFY_LOCAL_STORAGE_DIR to a directory inside this project, put your JSON input at key_value_stores/default/INPUT.json in that directory, then run npm start. Reuse that directory to retain named-store history. No Apify login or push is necessary. The Dockerfile uses apify/actor-node:22 with a separate build stage.
For API contracts and platform behavior, see Greenhouse Job Board API, Lever Postings API, Ashby public job postings API, SmartRecruiters Posting API, and Apify PPE documentation.