Company Career Jobs API: Greenhouse, Lever, Ashby & more avatar

Company Career Jobs API: Greenhouse, Lever, Ashby & more

Pricing

from $0.80 / 1,000 job delivereds

Go to Apify Store
Company Career Jobs API: Greenhouse, Lever, Ashby & more

Company Career Jobs API: Greenhouse, Lever, Ashby & more

Pricing

from $0.80 / 1,000 job delivereds

Rating

0.0

(0)

Developer

Richard Hutton

Richard Hutton

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

15 hours ago

Last modified

Share

Company Career Jobs API: Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters and Personio jobs

Company Career Jobs API keeps a recurring feed of open jobs for the companies you track, across Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters and Personio, so you don't have to maintain seven API clients, a seen-jobs store or failure handling yourself.

  • Input: board URLs, domains or company names. The Actor auto-detects the ATS and reads the public job-board API that ATS publishes for careers pages, with no logins or proxies.
  • Output: one normalized JSON record per job: title, department, team, locations, workplace type, employment type, published salary, dates, job and apply URLs, and description.
  • Monitoring: with onlyNewSinceLastRun, each scheduled run returns only jobs added since the previous run, lists removed postings, and explains every company in a COMPANY_STATUS record.

Use it as a careers-page jobs API, a hiring-signal feed for sales and recruiting, or a source for job boards and AI agents. It works from the Apify Console, the API, schedules, webhooks, Make, Zapier, n8n and the Apify MCP server.

Quickstart: a three-board feed in two runs

Open the ready-to-run example for a guided single run.

  1. Start the Actor with this input in the JSON tab:

    {
    "companies": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
    ],
    "onlyNewSinceLastRun": true,
    "stateStoreName": "my-three-board-feed",
    "maxJobsPerCompany": 3,
    "includeDescription": "none"
    }

    stateStoreName names the key-value store that remembers which jobs this feed has already seen. Apify keeps named stores until you delete them. Give every independent feed its own name. If you save the input as a Task, its history is also kept separate from other tasks.

  2. Run 1 is the baseline. It delivers up to 3 jobs per board, at most 9, and records every job currently on each board as seen. Event charges: at most 3 × $0.002 + 9 × $0.0008 = $0.0132.

  3. Run 2: start the same input again. It returns only jobs that were not on the boards during run 1. If the boards haven't changed, which is common for runs minutes apart, the dataset is empty and every company shows newJobs: 0. That run costs 3 × $0.002 = $0.006.

  4. Where to look:

    • The Output tab lists delivered jobs.
    • In the run's Storage → Key-value store, COMPANY_STATUS explains each company: ATS, jobs on the board, new jobs, delivered jobs and a message. On run 1 the message says "First run for this board: baseline saved".
    • REMOVED_JOBS lists postings that disappeared since the previous run.
    • RUN_SUMMARY holds the totals.
  5. Platform usage is included in the price; you pay only the event charges. To keep the feed running, save the input as a Task and add a schedule (see Monitoring new jobs on a schedule).

Maintainer QA, not a customer result: on 20 Oct 2026 the owner ran this input pattern on build 0.1.5 with its own state store. Run 1 returned 9 jobs from 3/3 companies in about 4 s, and the repeat returned 0 jobs from 3/3 companies in about 3 s.

Run it from the API (curl or Python)

Keep your Apify API token (Apify Console → Settings → API & Integrations) in the APIFY_TOKEN environment variable. The examples send it only in the Authorization header, never in the URL. maxTotalChargeUsd caps what a run can be charged, and timeout limits the run to 120 seconds. The synchronous endpoint waits at most 300 seconds and answers HTTP 408 if the run takes longer. For big lists, start the run with POST /v2/acts/Hd56tFCgqOeIdJUjV/runs and read the dataset when it finishes. Other errors: 400 invalid input, 401 missing or wrong token, 402 usage limit or credits exhausted, 403 insufficient permissions. If the connection times out or drops, the run may still exist. Do not blindly repeat the POST: first check the Actor's Runs tab and retrieve the existing result if it completed; use the asynchronous endpoint when you need a run ID for reliable recovery.

curl -sS --fail-with-body -X POST \
"https://api.apify.com/v2/acts/Hd56tFCgqOeIdJUjV/run-sync-get-dataset-items?maxTotalChargeUsd=0.05&timeout=120" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companies": ["https://boards.greenhouse.io/stripe"], "maxJobsPerCompany": 5, "includeDescription": "none"}'
import json
import os
import urllib.error
import urllib.request
ACTOR_ID = "Hd56tFCgqOeIdJUjV" # volanta-ai/company-career-jobs-api
URL = (f"https://api.apify.com/v2/acts/{ACTOR_ID}/run-sync-get-dataset-items"
"?maxTotalChargeUsd=0.05&timeout=120")
def fetch_jobs(companies, max_jobs_per_company=5):
body = json.dumps({
"companies": companies,
"maxJobsPerCompany": max_jobs_per_company,
"includeDescription": "none",
}).encode()
request = urllib.request.Request(URL, data=body, method="POST", headers={
"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}",
"Content-Type": "application/json",
})
try:
with urllib.request.urlopen(request, timeout=320) as response:
return json.load(response)
except urllib.error.HTTPError as error:
if error.code == 408:
raise RuntimeError("The run took longer than 300 s; use the asynchronous /runs endpoint.") from error
detail = error.read().decode("utf-8", "replace")[:300]
raise RuntimeError(f"Apify API error {error.code}: {detail}") from error
if __name__ == "__main__":
for job in fetch_jobs(["https://boards.greenhouse.io/stripe"]):
print(job["companyName"], "|", job["title"], "|", job["jobUrl"])

What it does

  • 7 ATS in one Actor: Greenhouse, Lever (US and EU), Ashby, Workable, Recruitee, SmartRecruiters, Personio (.de and .com).
  • Auto-detection: paste a board URL (https://jobs.lever.co/palantir), a domain (ramp.com), a slug (stripe) or a name (Hugging Face). Known URL patterns are parsed directly; otherwise at most 3 candidate slugs are checked against each ATS API (stopping at the first board with jobs), and as a fallback the company website is read once to find the ATS link. Detections are cached between runs. Links to job sites and social networks (a LinkedIn company page, an Indeed listing...) and unsupported ATS (Workday, iCIMS, Taleo...) are rejected with a clear message instead of guessed.
  • One normalized schema for all ATS: title, department, team, locations, workplace type (remote / hybrid / onsite), employment type, salary range when the employer publishes it, posting and update dates, job and apply URLs, description as plain text and/or HTML.
  • Filters: include and exclude title keywords, locations, remote only, departments or teams, posted within N days, max jobs per company.
  • New-jobs monitoring: onlyNewSinceLastRun remembers seen jobs per job board, returns only new postings on each scheduled run and lists removed (closed) jobs.
  • Per-company status: a COMPANY_STATUS record tells you, for every input, which ATS was detected, how many jobs were found and delivered, and why a company returned nothing.
  • Contact-detail redaction (best effort): recruiter and hiring-manager fields from the ATS APIs are not included in the output, and email addresses, phone numbers and contact-person names that follow labels such as "Contact person:" or "Ansprechpartnerin:" are replaced in descriptions. Redaction is pattern based, so free-text descriptions can still contain personal information.

Who it is for

  • Sales and recruiting teams tracking hiring signals: which target accounts open roles in Engineering, Sales or a new country.
  • Job boards, newsletters and aggregators that need fresh, structured jobs straight from company career pages.
  • Market and talent researchers analysing hiring volume, locations, remote share and published salary ranges.
  • Developers and AI agents that need a reliable "get open jobs for company X" tool with a stable output schema.

Supported ATS and accepted inputs

ATSExample inputSalary dataNotes
Greenhousehttps://boards.greenhouse.io/stripe, https://job-boards.greenhouse.io/stripe, embed ?for=stripeNo (not in the public list API)Department from the board
Leverhttps://jobs.lever.co/palantir, https://jobs.eu.lever.co/<company>Yes, when published (salaryRange)US and EU hosts; company name derived from the slug
Ashbyhttps://jobs.ashbyhq.com/rampYes, when publishedDepartment, team, workplace type
Workablehttps://apply.workable.com/huggingface, https://<company>.workable.comNoRemote flag
Recruiteehttps://bunq.recruitee.comYes, when publishedRemote / hybrid / on-site flags
SmartRecruitershttps://jobs.smartrecruiters.com/Ubisoft2, https://careers.smartrecruiters.com/<company>NoDescriptions need one extra request per job
Personiohttps://tado.jobs.personio.de, https://<company>.jobs.personio.comYes, when publishedXML feed; legal entity as company name

Also accepted: a careers page URL (https://example.com/careers), a domain (ramp.com), a slug (stripe) or a company name (Hugging Face). Board URLs are the fastest and most precise input.

Not supported: Workday, iCIMS, Taleo, SuccessFactors and other systems without an official public job-board API.

Input example

{
"companies": [
"https://boards.greenhouse.io/stripe",
"https://jobs.lever.co/palantir",
"ramp.com",
"Hugging Face"
],
"titleKeywords": ["engineer", "data scientist"],
"excludeKeywords": ["intern"],
"locations": ["London", "Remote"],
"postedWithinDays": 30,
"includeDescription": "text",
"maxJobsPerCompany": 100,
"onlyNewSinceLastRun": false
}
FieldTypeDefaultDescription
companiesarray of stringsrequiredBoard URLs, careers page URLs, domains, slugs or names. Max 1,000 per run, 500 characters per entry.
atsHintstringautoForce one ATS (greenhouse, lever, ashby, workable, recruitee, smartrecruiters, personio) for slug/name/domain inputs.
titleKeywordsarray[]Keep titles containing any keyword. Whole-word, case-insensitive; engineer* = prefix match.
excludeKeywordsarray[]Drop titles containing any keyword (same rules).
locationsarray[]Keep jobs whose location contains any text (substring). remote also matches remote jobs.
remoteOnlybooleanfalseKeep only remote jobs.
departmentsarray[]Keep jobs whose department or team contains any text.
postedWithinDaysinteger0Keep jobs posted in the last N days (0 = no limit).
includeDescriptionstringtexttext, html, both or none.
maxJobsPerCompanyinteger0Max jobs delivered per company, newest first (0 = no limit; Free plan demo: 100).
onlyNewSinceLastRunbooleanfalseReturn only jobs not seen in the previous run; list removed jobs.
stateStoreNamestringats-jobs-feed-stateNamed key-value store for seen jobs and detection cache. Runs from a saved task already keep their own history; set a different name only for independent monitors that run without a task.
scanCareersPagebooleantrueRead the company website once (respecting robots.txt) when slug detection fails.

Output example

One dataset item per job (a real record from a live run, description shortened; includeDescription: "text"):

{
"jobId": "ashby:190c265a-ca4f-48b7-b2a4-13412299249d",
"ats": "ashby",
"nativeId": "190c265a-ca4f-48b7-b2a4-13412299249d",
"companySlug": "ramp",
"companyName": "Ramp",
"title": "Manager, Account Manager | Growth",
"department": "Sales",
"team": "Account Manager",
"locations": ["New York, NY (HQ)", "San Francisco, CA"],
"workplaceType": "hybrid",
"employmentType": "full-time",
"employmentTypeRaw": "FullTime",
"salaryMin": 180000,
"salaryMax": 247000,
"salaryCurrency": "USD",
"salaryPeriod": "year",
"postedAt": "2026-10-13T15:07:59.917Z",
"updatedAt": null,
"jobUrl": "https://jobs.ashbyhq.com/ramp/190c265a-ca4f-48b7-b2a4-13412299249d",
"applyUrl": "https://jobs.ashbyhq.com/ramp/190c265a-ca4f-48b7-b2a4-13412299249d/application",
"descriptionText": "About Ramp\n\nRamp is building the smart infrastructure for finance teams, embedded in the transaction flow of every dollar a business spends. ...",
"descriptionHtml": null,
"isNew": true,
"firstSeenAt": "2026-10-14T14:11:48.308Z",
"fetchedAt": "2026-10-14T14:11:48.308Z",
"companyInput": "ramp.com"
}

Every field is always present; unknown values are null. Field notes:

  • jobId is stable across runs: <ats>:<native id>. Use it to deduplicate.
  • workplaceType: remote, hybrid, onsite or null. Taken from the ATS when it has a field for it, otherwise inferred from "Remote" or "Hybrid" in the location or title, otherwise null.
  • employmentType: full-time, part-time, contract, temporary, internship, apprenticeship, freelance, volunteer or null; the ATS value is kept in employmentTypeRaw.
  • salaryMin, salaryMax, salaryCurrency, salaryPeriod (year, month, week, day, hour) are filled only when the employer publishes pay in the ATS.
  • salaryMin / salaryMax are copied as published by the employer, without corrections.
  • isNew / firstSeenAt: whether the job was absent in the previous run for this board (in the same task), and when this Actor first saw it. On the first run every job is new.
  • descriptionText / descriptionHtml: filled according to includeDescription; emails, phone numbers and labelled contact-person names are replaced, on a best-effort basis, with [email removed], [phone removed] and [name removed]. descriptionHtml is lightly sanitized (scripts, iframes, forms, event handlers and javascript:/mailto: links removed); run it through your own HTML sanitizer before rendering it on a website.

Default key-value store records:

  • COMPANY_STATUS: one entry per input with status (ok, not_found, error, duplicate, skipped), ats, boardUrl, detectionMethod (url, probe, hint, careers-page, cache), jobsOnBoard, jobsMatched, newJobs, jobsPushed, removedJobsCount, removedJobs, message and the detection attempts.
  • REMOVED_JOBS: jobs that were on a board in the previous run and are gone now (jobId, title, jobUrl, firstSeenAt, company).
  • RUN_SUMMARY: totals for the run.

Example COMPANY_STATUS entry (real, from a run with maxJobsPerCompany: 5):

{
"input": "ramp.com",
"status": "ok",
"ats": "ashby",
"companySlug": "ramp",
"companyName": "Ramp",
"boardUrl": "https://jobs.ashbyhq.com/ramp",
"detectionMethod": "probe",
"jobsOnBoard": 162,
"jobsMatched": 162,
"newJobs": null,
"jobsPushed": 5,
"removedJobsCount": null,
"removedJobs": [],
"charged": true,
"message": "First run for this board: baseline saved, all jobs count as new. 157 more matching jobs were not delivered because of maxJobsPerCompany.",
"attempts": [
"recruitee:ramp (not found)",
"smartrecruiters:ramp (not found)",
"workable:ramp (found, 0 jobs)",
"personio:ramp (not found)",
"greenhouse:ramp (not found)",
"ashby:ramp (found, 162 jobs)",
"lever:ramp (not found)"
],
"checkedAt": "2026-10-14T14:11:48.323Z"
}

Monitoring new jobs on a schedule

  1. Save your input with onlyNewSinceLastRun: true as a task and run it once to set the baseline (see the Quickstart).
  2. Add a schedule, for example daily. Each run returns only new jobs, and REMOVED_JOBS lists closed postings.
  3. Connect a webhook, Slack, email, Google Sheets, Make, Zapier or n8n integration to act on new jobs.

Seen jobs are stored per job board in the named key-value store set by stateStoreName (default ats-jobs-feed-state), so two spellings of the same company share one history. Per-run records (COMPANY_STATUS, REMOVED_JOBS, RUN_SUMMARY) live in each run's own key-value store, which Apify deletes after your plan's retention period. A job that disappears is reported in REMOVED_JOBS once; if it comes back within 30 days (for example after a temporary unpublish), it is not delivered or charged as new again. Each saved task keeps its own history, so two monitoring tasks, or a monitoring task and a quick test run from the Console, never use up each other's new jobs. Runs started without a task (for example directly from the API) share one history per stateStoreName; give independent API monitors different stateStoreName values. The first run saves the whole board as the baseline, including jobs it did not deliver because of maxJobsPerCompany or a limit, so later runs only return jobs that were really added after it. From then on, a new job that matched your filters but was not delivered (because of maxJobsPerCompany, the Free plan demo limit or your spending limit) is not marked as seen, so it is delivered in the next run.

Pricing

This Actor uses pay-per-event pricing. Platform usage is included, so you pay only for these events:

EventPrice (USD)When it is charged
Company checked (company-checked)$0.002Once per company whose job board was found and read, including boards that currently list no jobs (you still get its status and removed jobs). Not charged for companies that are not found, API outages, duplicates, or companies skipped by the demo cap.
Job delivered (job)$0.0008Once per job record in the dataset, after your filters. Filtered-out jobs are free.

Examples:

  • 10 companies, 50 matching jobs each: 10 x $0.002 + 500 x $0.0008 = $0.42.
  • Daily monitor of 100 companies with onlyNewSinceLastRun, about 3 new matching jobs per company per day: $0.20 + $0.24 = $0.44 per day.
  • Checking 1,000 companies with maxJobsPerCompany: 5: $2 + up to $4 = at most $6.

The run stops cleanly when your maximum charge per run is reached: no company or job is started or pushed that would exceed it, and every job in the dataset is charged exactly once. Companies that were not reached are listed as skipped in COMPANY_STATUS.

Free plan demo

On the Apify Free plan, each run is a demo: it checks the first 10 companies of your list and returns up to 100 jobs per company. Skipped companies are listed as skipped in COMPANY_STATUS, and the run log explains the limit. Any paid Apify plan removes both limits.

FAQ

Why did a company return no jobs? Open the COMPANY_STATUS record. not_found means no public board was found on the seven supported ATS for that input (paste the board URL from the company's careers page, or set atsHint), or that the input was a job site such as LinkedIn or an unsupported ATS such as Workday (the message says which). ok with 0 jobs means the board exists but lists no open roles or none matched your filters (jobsOnBoard vs jobsMatched).

I have two scheduled monitors. Do they interfere? No, as long as each runs from its own saved task: every task keeps its own seen-jobs history. For monitors started directly through the API without a task, set a different stateStoreName per monitor.

The wrong ATS or company was detected. Slugs such as apple can exist on several systems. Paste the exact board URL, or set atsHint. Board URLs are never guessed. For multi-word names, the shortest guess (the first word, e.g. acme for "Acme Robotics") is only accepted when the ATS reports a matching company name.

How fresh is the data? Every run reads the live ATS API. There is no intermediate database or cache of job data.

Does it return salaries? Yes, when the employer publishes pay ranges in Ashby, Lever, Recruitee or Personio. Greenhouse, Workable and SmartRecruiters do not expose pay in their public list APIs, so those fields are null.

Can I get Workday, iCIMS or LinkedIn jobs? No. This Actor reads the public job-board APIs that ATS vendors provide for displaying jobs on careers pages. Workday, iCIMS and LinkedIn do not offer an equivalent public jobs API, so they are not supported.

How do AI agents use it? Through the Apify MCP server or the Apify API: call the Actor with companies (and optional filters) and read the dataset. The output schema is fixed and documented above, so agents can rely on field names and types.

How long does a run take? A board URL needs one request (SmartRecruiters: one per 100 postings). Domains and names are checked against up to 9 ATS API endpoints in parallel for each candidate slug (at most 3 slugs), stopping at the first board with jobs. SmartRecruiters descriptions need one extra request per delivered job. Typical runs of a few companies finish in seconds.

Limits

  • Up to 1,000 companies per run; split bigger lists into several runs or tasks.
  • SmartRecruiters: up to 10,000 postings per company (auto-detection reads only the first page; the full listing is loaded once the board is chosen); descriptions add one request per delivered job, so use maxJobsPerCompany or includeDescription: "none" for very large boards. SmartRecruiters answers unknown company identifiers with an empty list, so a mistyped board URL shows up as a board with 0 jobs (the status message says so).
  • Lever's and Ashby's public APIs do not return the company name; it is taken from your input name or derived from the board slug (palantir becomes Palantir).
  • Greenhouse EU-hosted boards (job-boards.eu.greenhouse.io) are tried on the global API first, then the EU API.
  • Descriptions are returned in the language the employer published them in.
  • Detection by name or domain is best effort. Board URLs are always exact.
  • Data sources: Greenhouse Job Board API, Lever Postings API, Ashby Job Postings API, Workable job widget API, Recruitee Careers Site API, SmartRecruiters Posting API and the Personio XML job feed. These endpoints are published by the ATS vendors so that job postings can be displayed and syndicated outside their own sites. For detection by domain or name, the Actor may also read the company's own careers page once.
  • Access: no logins, CAPTCHA solving or bot-detection workarounds are used. The optional careers-page read happens only when robots.txt allows it.
  • Requests identify themselves with the User-Agent VolantaActor/0.1 (+mailto:richard@volanta.ai), use timeouts and back off on rate limits.
  • Personal information (best effort): recruiter, hiring-manager and contact fields from the ATS APIs are not included in the output (for example the SmartRecruiters creator or the Recruitee mailbox_email). Email addresses, phone numbers and names that follow a contact label are redacted from job descriptions. Redaction is pattern based, so descriptions written by employers can still contain personal information; review them before republishing.
  • Job texts remain the property of the employers. You are responsible for using the data in line with applicable law and the employers' terms, for example when republishing descriptions.

Integrations and API

Run it from the Apify API, JavaScript or Python client, the Apify CLI, schedules, webhooks, or integrations such as Make, Zapier, n8n, Google Sheets and Slack. Results can be downloaded as JSON, CSV, Excel, XML or RSS.

Support

Built and maintained by Volanta (Volanta S.R.L., Romania). Questions, bugs or a missing ATS: richard@volanta.ai, or open an issue on the Actor's Issues tab.

For maintainers

No API keys or secrets are required: all data sources are public APIs. Environment variables:

VariablePurpose
APIFY_USER_IS_PAYINGSet by the Apify platform (1 = paying user). Anything else on the platform enables the Free plan demo limits (10 companies per run, 100 jobs per company; constants in src/main.js). Locally the limits apply only if you set it to something other than 1.
ACTOR_TASK_IDSet by the platform for task runs; scopes the seen-jobs history to the task.
ATS_FEED_OFFLINE=1Mock mode: serve recorded fixtures from test/fixtures instead of calling the ATS APIs. Uses the state store <stateStoreName>-offline, so it never touches a live baseline.
CRAWLEE_STORAGE_DIRLocal storage folder (default ./storage); point it to a temp folder for throwaway runs.
NODE_USE_ENV_PROXY=1Local only, when your network requires an HTTP(S) proxy.
APIFY_ACTOR_PRICING_INFO, APIFY_CHARGED_ACTOR_EVENT_COUNTS, ACTOR_MAX_TOTAL_CHARGE_USD, ACTOR_USE_CHARGING_LOG_DATASET=1Local PPE simulation (charges are logged to the charging_log dataset). Locally the SDK counts every event as $1 against the limit but adds real prices to the total, so budget stops are not realistic; test/billing.test.js checks budget handling with the SDK's real ChargingManager in platform mode instead.

Commands:

npm install
npm test # unit tests (no network, recorded fixtures)
ATS_FEED_OFFLINE=1 npm start # offline end-to-end run (input in storage/key_value_stores/default/INPUT.json)
apify run # live local run
node scripts/record-fixtures.mjs # re-record trimmed fixtures from the live APIs
node scripts/validate-schemas.mjs [items.json] # validate .actor schemas (and dataset items) with Apify's validators

Pricing events are defined in .actor/pay_per_event.json and charged in src/billing.js. When you set up pay-per-event pricing in the Console, delete the synthetic apify-default-dataset-item event that Apify adds by default: the job event already charges once per dataset item, so keeping it would make users pay twice per job (the code would still keep the total within the user's max charge, but every job would cost more than the price table above). The seen-jobs state and detection cache live in the named key-value store from stateStoreName; if the Actor runs with limited permissions and cannot open it, the run continues without new-job tracking and logs a warning.