Greenhouse, Lever & Ashby Jobs API with New-Job Alerts
Pricing
from $1.50 / 1,000 jobs
Greenhouse, Lever & Ashby Jobs API with New-Job Alerts
Job postings API for company career sites on Greenhouse, Lever, Ashby, Workable and SmartRecruiters. Give it a company name or domain. You get normalised location, country and salary, a country filter, and alerts for new and removed jobs between runs.
Pricing
from $1.50 / 1,000 jobs
Rating
0.0
(0)
Developer
fikri marhan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
17 days ago
Last modified
Categories
Features
Share
Get every open job straight from company career sites, plus alerts when jobs are added or removed. Give it company names, domains or job-board links. It finds the company's applicant tracking system (ATS) and returns clean, normalised job data from the platform's official public job-board API. No login, cookies or proxies are needed.
Supported platforms: Greenhouse, Lever, Ashby, Workable, SmartRecruiters.
Use cases
- Hiring signals and lead generation: see which companies are hiring, for which roles and in which countries.
- Job boards and aggregators: a fresh, structured feed from hundreds of company career pages.
- Recruiting and talent intelligence: track competitors' openings, departments and pay ranges.
- Job alerts: schedule it daily in "Only new jobs" mode and send the new postings to Slack, email or Google Sheets.
- Job market research by country: for example, all tech roles in Malaysia, Singapore or Indonesia from top employers.
One job postings API for five job board platforms
Greenhouse, Lever, Ashby, Workable and SmartRecruiters each have their own public job board API with different fields, pagination and URL schemes. This Actor calls each official public job board API (for example the Greenhouse Job Board API and the Lever Postings API) and returns one consistent schema. You don't need an API key, and it doesn't scrape HTML.
Features
- Auto-detection: give it
notion.soand it checks the company's careers page for its ATS. Or give it a slug likestripeand it probes each platform in turn. - Normalised locations: city, ISO country code, country name, all countries for multi-location jobs, regions (EMEA, APAC, LATAM…) and a remote flag.
- Standard employment type: every job gets one of
FULL_TIME,PART_TIME,CONTRACT,TEMPORARY,INTERN,FREELANCE,VOLUNTEERorOTHER(ornullwhen the board doesn't say), whatever the platform calls it. The original text is kept inemploymentTypeRaw. See Employment types below. - Normalised salary: min, max, currency and interval (
YEAR,MONTH,WEEK,DAYorHOUR). It uses the structured pay data from Greenhouse, Lever and Ashby when present, and otherwise conservatively parses pay ranges from the description. - Country filter: ISO codes or country names, such as
MY,SGorUnited States, with an optional "remote, no country" setting. Regions such asEurope,EMEAorAPAC, andRemote, aren't accepted as filters: list the countries instead, or use Remote jobs only. Unrecognised values are ignored with a warning. If none of the values is a recognised country, no company is looked up, nothing is charged, and the run finishes with one freeINVALID_INPUTerror record per value. - Change monitoring: each job is marked
new,existingorremovedsince your last run. The Actor remembers jobs in a key-value store in your account (see Where the memory of seen jobs is kept below). Modes: all jobs, only new jobs, or changes (new + removed). - Privacy-friendly: no recruiter or hiring-manager names are collected, and emails and phone numbers are removed from descriptions.
- Polite: requests to each host are spaced out, with retries and backoff that respect
Retry-After. When detecting from a domain it fetches at most three pages and respectsrobots.txt.
Input examples
Track all Singapore and Malaysia jobs at a few companies:
{ "companies": ["stripe", "openai", "smartrecruiters:BoschGroup"], "countries": ["SG", "MY"] }
Daily alert of new engineering jobs only:
{ "companies": ["notion.so", "lever:spotify", "huggingface"], "titleKeywords": ["engineer"], "outputMode": "newOnly", "baselineFirstRun": true, "monitorId": "eng-watch" }
Run it through the API
Send the input as a JSON body. companies must be a JSON list of strings, even for one company: ["stripe"], not "stripe". Apify checks the input before the run starts, and a wrong type or value returns HTTP 400 with a message such as Field input.companies must be array. No run is created and nothing is charged.
Run and get the jobs in one call (waits up to 5 minutes):
curl -X POST "https://api.apify.com/v2/acts/neorhapsody~ats-jobs-feed/run-sync-get-dataset-items" \-H "Authorization: Bearer <YOUR_API_TOKEN>" -H "Content-Type: application/json" \-d '{"companies": ["stripe", "lever:spotify"], "maxJobsPerCompany": 20}'
To keep the memory of seen jobs in a store that already exists in your account, send its ID or name in stateStore, for example {"companies": ["stripe"], "outputMode": "newOnly", "stateStore": "my-existing-store"}. Apify then gives the run access to that store. Sending the same name in stateStoreName doesn't, because the Actor may only open stores it created itself (see Where the memory of seen jobs is kept).
- In REST URLs, the Actor name uses
~:neorhapsody~ats-jobs-feed. A/there returns 404. run-sync-get-dataset-itemsreturns the dataset rows (jobs plus any error records).run-syncreturns the run'sOUTPUTrecord instead: counts, warnings by code and a link to the dataset. For long runs, start withPOST …/runsand read the dataset when the run finishes.- If a sync call returns
run-failed, open the run (its ID is in the message) and check its status message orRUN_SUMMARY. - The JavaScript, Python and CLI examples on the API tab work as shown.
Output example
{"platform": "ashby", "companySlug": "openai", "companyName": "Openai", "jobId": "b71906c2-...","title": "Quality Engineer, Rack Infrastructure & Site Operations", "department": "Scaling","employmentType": "FULL_TIME", "employmentTypeRaw": "FullTime","workplaceType": "hybrid", "isRemote": false, "location": "Singapore", "city": "Singapore","countryCode": "SG", "countryName": "Singapore", "countryCodes": ["SG"],"salaryMin": 171000, "salaryMax": 345000, "salaryCurrency": "SGD", "salaryInterval": "YEAR", "salarySource": "structured","postedAt": "2026-09-09T18:22:22.536Z", "url": "https://jobs.ashbyhq.com/openai/...", "changeType": "new", "firstSeenAt": "2026-10-22T14:00:00.000Z"}
description appears only when Include job description is on. Rows with changeType: "removed" also have removedAt, and carry only the identifying fields (company, job ID, title, URL, countries, remote flag, first seen). changeType is new, existing or removed, and is present in every output mode. applyUrl is the same as url when the platform has no separate application page.
You can download results as JSON, CSV or Excel, or read them through the API. The RUN_SUMMARY record in the run's key-value store shows per-company counts, error counts by code, warnings and the events charged. Inputs that couldn't be read appear as error records in the dataset (see Troubleshooting).
Employment types
employmentType uses the same values on every platform. They're based on Google's and schema.org's JobPosting employmentType, with CONTRACT for Google's CONTRACTOR and a separate FREELANCE:
| Value | Typical platform text |
|---|---|
FULL_TIME | Full-time, FullTime, Full Time, Permanent, Regular, Employee, Temps plein |
PART_TIME | Part-time, PartTime |
CONTRACT | Contract, Contractor, Contingent Worker, Third Party Contract |
TEMPORARY | Temporary, Temp, Fixed-term, Short Term, Seasonal, Casual, Temp to Perm |
INTERN | Intern, Internship, Apprenticeship, Trainee, Working Student |
FREELANCE | Freelance |
VOLUNTEER | Volunteer |
OTHER | Published, but none of the above, e.g. "Other", "Full Time/Part Time" or a program name |
null | Not published, or the field only says where people work ("Remote", "Hybrid", "Onsite") |
When several apply, the more specific one wins, in this order: intern, volunteer, freelance, temporary (so "Full-time fixed-term" is TEMPORARY), contract ("Permanent contract" still counts as FULL_TIME), part-time, full-time.
employmentTypeRaw keeps the platform's own text, so you can always apply your own rules.
Sources by platform:
- Ashby:
employmentType - Lever:
commitment, which companies fill in freely and which sometimes holds a location or work mode - Workable:
employment_type - SmartRecruiters:
typeOfEmployment - Greenhouse: no standard field, so the Actor reads a custom Employment Type field when the board publishes one. Otherwise it's
null.
Need it as a REST API instead?
The same data is available as the ATS Jobs API on RapidAPI: https://rapidapi.com/fikrimarhan/api/ats-jobs-api4 (Greenhouse, Lever, Ashby, SmartRecruiters). Use it when you want live job lists from your own code with one HTTP request, instead of scheduled runs and datasets.
Pricing
Pay per event, with no extra platform-usage charges:
- $1.50 per 1,000 jobs (
job, $0.0015 per job written to the dataset) - $0.005 per company looked up (
company-lookup, charged once per company whose job board was actually read; inputs where no board is found are free)
Apify also adds its standard Actor start event ($0.00005 per run at the default 256 MB).
Jobs removed by your filters, or by "Only new jobs" mode, are free. A daily alert on 50 companies that yields 20 new jobs costs about $0.28 per run. If you set a maximum cost per run, the actor stops cleanly at the limit and doesn't advance the monitor state for a board it couldn't deliver in full, so nothing is lost. Error records are never charged.
How do I find a company's Greenhouse, Lever or Ashby slug?
Open the company's careers page and click a job. The slug is in the job board URL:
boards.greenhouse.io/airbnborjob-boards.greenhouse.io/airbnb→greenhouse:airbnbjobs.lever.co/spotify→lever:spotifyjobs.ashbyhq.com/ramp→ashby:rampapply.workable.com/huggingface→workable:huggingfacejobs.smartrecruiters.com/BoschGroup→smartrecruiters:BoschGroup
You can also paste the full board URL or just the company's domain, and the Actor will find the board itself.
Tips
- Explicit
platform:sluginputs are the fastest and most reliable. Domain detection works when the careers page links to the ATS. - Use one Monitor ID per schedule, so each watch list remembers its own jobs.
- SmartRecruiters boards can be huge. The Actor reads at most 2,000 postings per SmartRecruiters company, and Max jobs per company lowers that cap. The cap applies before your filters, so with country or title filters, leave it at 0 or set it high. A
BOARD_TRUNCATEDwarning tells you when a board was cut. - State store name: usually leave it as is. If you change it, use only letters, digits and hyphens (not at the start or end), up to 63 characters, for example
my-job-alerts. An empty value means the default store.
Where the memory of seen jobs is kept
To know which jobs are new, the Actor saves the jobs it has seen in a key-value store in your Apify account. There are two ways to choose that store:
- State store name (
stateStoreName, defaultneorhapsody-ats-jobs-feed-state-v2). The Actor creates a store with this name on its first run and reuses it on every later run. Most people never need to change it. Use Monitor ID to keep separate watch lists in the same store. - State store (pick an existing one) (
stateStore). Select a store that already exists in your account, for example one you made yourself or one another tool uses. When this field is set, State store name is ignored.
Why the picker exists. The Actor runs with Apify's limited permissions. It can use stores it created itself, and stores you select in the picker, but nothing else in your account. So if a store with the name you type already exists and the Actor didn't create it, Apify won't let the Actor open it.
What happens then. The run doesn't fail. The Actor uses its own replacement store instead, named after the one you asked for plus -own (for example my-alerts-own), and it uses the same replacement on later runs. The run shows a STATE_STORE_FALLBACK warning in its status message, log and RUN_SUMMARY. On the first such run the replacement store is empty, so every job counts as new, and in Only new jobs or Changes mode each one is output and charged once. After that, it works normally. To avoid this, select the store in the picker, or type a name you haven't used yet. If even the replacement store can't be opened, the run fails with STATE_STORE_ERROR and says what to do.
RUN_SUMMARY.stateStore shows which store the run used: source is name, picker or fallback.
Is this legal?
The actor reads the same public, unauthenticated job-board APIs that companies' own careers pages use to show their openings. It collects job postings, not personal data. You're responsible for how you use and republish the data; please link back to the original posting.
Error reports
When a run is about to end as failed, the Actor sends one small, anonymous error report to the developer, so that bugs can be found and fixed. Apify doesn't show developers other users' runs, so this is the only way we learn why a run failed.
What is sent (one report per failed run, at most 2 KB):
- the Actor build number, the time, and the stage where the run failed (for example
validateorcompanies) - the error type (for example
TypeError), its code (for exampleINTERNAL_ERROR), and the error message after cleaning: URLs, email addresses,platform:slugpairs, quoted text, file paths, long IDs and every value from your input are removed, and it's cut to 300 characters - the run's memory size in MB and how it was started (for example
APIorSCHEDULER) - the coarse shape of your input: the number of companies as a range (for example
2-5), which platforms were selected, which filters were used (true or false only), and your maximum charge as a range - platform signals seen during the run: a migration, an abort, or high CPU or memory use
If memory use passes 90% of the run's limit (the usual sign of an out-of-memory crash), one report of the same kind is also sent, once per run, because a crash like that can't be reported afterwards. One report of the same kind is also sent when the run had to use a replacement state store (STATE_STORE_FALLBACK), so we can see how often that happens. It never contains the store names.
What is never sent: company names, slugs, domains or URLs; title keywords, countries or any other input value; job data; Monitor ID or store names; your user ID, run ID, IP address or API token. The receiving server doesn't store IP addresses or request headers.
Retention: reports are deleted automatically after 90 days.
Opt out: set Turn off error reports ("disableErrorReports": true) in the input. Nothing is sent then. No report is sent for successful runs, or if the run fails before its input could be read.
Troubleshooting
Rejected before the run starts (HTTP 400, no run, no charge). Apify checks the input against the input schema. Common causes:
companiesis missing, empty, or not a listcountries,platformsor keywords aren't lists- an unknown
outputModeor platform name (platform names are lowercase) - an empty
platformslist - a blank Monitor ID
- a State store name with characters other than letters, digits and hyphens
- numbers outside their allowed range
The response names the field.
Error records (the run still succeeds). One bad company never stops the run. Each problem becomes an error record in the dataset with isError: true, an errorCode, a plain-English errorMessage and the offending input. The Errors view shows these columns. Error records are free. Counts per code are in RUN_SUMMARY.
Warnings are listed in RUN_SUMMARY.warningsByCode and in the run log.
Failed runs should only happen for the INTERNAL_ERROR and STATE_STORE_ERROR cases below, or when Apify stops the run (timeout or out of memory). The coded reason is in the run's status message and in RUN_SUMMARY.fatalError.
| Code | Meaning | What to do |
|---|---|---|
INVALID_INPUT | Error record: the company input isn't a slug, platform:slug, board URL or domain, or none of the countries values is a recognised country (one record per value; no company is looked up). Warning: some countries values were ignored, or a blank Monitor ID or State store name was replaced by the default. | Fix the value named in the message. |
BOARD_NOT_FOUND | No public Greenhouse/Lever/Ashby/Workable/SmartRecruiters board was found (or only empty boards with that name). | Use the exact board URL or platform:slug. The company may use another ATS (e.g. Workday, which isn't supported). |
BOARD_NOT_PUBLIC | The board API refused access (HTTP 401/403). | The board is private or disabled. |
RATE_LIMITED | The platform kept returning HTTP 429 after retries. | Try later, or raise Delay between requests. |
UPSTREAM_ERROR | The platform returned a server error after retries. | Usually temporary. Re-run later. |
TIMEOUT / NETWORK_ERROR | The platform didn't respond. | Re-run later. |
UPSTREAM_SCHEMA_CHANGED | The platform's API no longer returns the expected fields. Also logged as a warning when only some jobs are affected; those jobs are skipped. | Please report it, so the parser can be updated. |
BOARD_EMPTY (warning) | The board exists but has no open jobs. | None. |
BOARD_TRUNCATED (warning) | A SmartRecruiters board has more postings than were read (2,000 cap, or your Max jobs per company). Jobs beyond the cap are missing, and removed-job detection is skipped for that board. | Set Max jobs per company to 0, or accept the cap. |
MAX_CHARGE_REACHED (warning) | Your maximum cost per run was reached; the remaining companies were skipped. | Raise the limit. |
NO_BOARDS_READ (warning) | None of the inputs could be read. The run still finishes as succeeded, with one free error record per input explaining why, and no lookup or job fees. | See the error records for each input (or RUN_SUMMARY, where noBoardsRead is true). |
STATE_STORE_FALLBACK (warning) | The store in State store name exists in your account, but the Actor didn't create it, so it isn't allowed to open it. The run used its own replacement store <name>-own instead. On the first such run every job counts as new and is charged once. | Select the store in State store (pick an existing one), or set State store name to the replacement name (or a new name) to stop the warning. |
STATE_STORE_ERROR (failed run) | The state store couldn't be opened: the name is invalid, the store you picked is gone, or neither the store you named nor its replacement could be opened. | Follow the message: select the store in State store (pick an existing one), type a name you haven't used yet, or leave State store name empty for the default. |
INTERNAL_ERROR (failed run) / FETCH_FAILED (error record) | An unexpected bug. | Please report it (see below). |
Found a bug or a wrong result? Open an Issue on this Actor's page in Apify Store (the Issues tab) and describe what you expected. Please paste the run URL into the issue: Apify then shares that run (log, input, dataset and RUN_SUMMARY) with the developer automatically, and only that run. Alternatively, enable Settings → Login & Privacy → Share run data with developers for this Actor. Apart from the small error report below, nothing leaves your run storage.