# Greenhouse, Lever, Ashby & Workable Jobs Scraper (`heyitsgautham/career-site-jobs-scraper`) Actor

Give it company websites like stripe.com, company names, or career page and job board URLs. It looks up the Greenhouse, Lever, Ashby, Workable, SmartRecruiters or Recruitee board and returns every open job with title, locations, remote flag, salary, description. $1.50 per 1,000 jobs, failures free.

- **URL**: https://apify.com/heyitsgautham/career-site-jobs-scraper.md
- **Developed by:** [Gautham Krishna](https://apify.com/heyitsgautham) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.90 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Greenhouse, Lever, Ashby & Workable Jobs Scraper

Get every open job from a company's own career site, straight from its applicant tracking system.
Paste company websites (`stripe.com`), company names (`Scale AI`), career page URLs, job board URLs or board names, and get one row per job with title, department, locations, remote flag, employment type, seniority level, dates, apply link, salary when published, and the full description.
For a website or a name the scraper finds the company's board itself, and every row says which board it used.

It reads Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee boards.
These systems publish a public JSON feed for each company board, so the scraper needs no login, no browser and no proxy.

You pay **$1.50 per 1,000 jobs** ($0.0015 each), and less on Apify Store discount tiers (Bronze, Silver, Gold), down to $0.90 per 1,000 on Gold.
Boards that don't exist, have no open jobs or fail are **never charged**, and finding a company's board from its website or name costs nothing.

### At a glance

| Topic | Details |
|---|---|
| Input | Many companies per run: company websites, company names, career page URLs, job board URLs or board names on Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee. Filters: keywords, excluded words, locations, remote only, posted within N days, salary listed, minimum salary, seniority (intern, entry level and up) |
| Output | One JSON row per open job: title, department, locations, remote flag, employment type, seniority level, dates, apply link, salary when published and the full description |
| Price | $0.0015 per job ($1.50 per 1,000), or $1.35 per 1,000 on Bronze, $1.10 on Silver and $0.90 on Gold. No start fee. Boards that don't exist, have no open jobs or fail are free, and so are jobs your filters drop |
| Tested | Board URLs: 61 of 61 real boards across all six systems returned jobs, 7,898 in total, in about 3 minutes. Websites and names: 238 real companies in three runs, see [Company websites and names](#company-websites-and-names) |

### What you get per job

| Field | What it holds |
|---|---|
| `status` | `ok` for a job. Other rows say why a board gave nothing, see below |
| `company`, `ats`, `boardSlug` | Company name, which ATS hosts the board, and the board name on that ATS |
| `boardUrl`, `resolvedBy`, `resolvedFrom` | Only for inputs the scraper resolved (websites, names, career pages, bare board names). The board used, how it was found, and the page that linked to it. Absent for board URLs and `ats:name` pairs |
| `jobId`, `title` | The ATS's own job ID and the job title |
| `department`, `team` | As the company labels them. Not every ATS has both |
| `location`, `locations` | First location, and every location the job lists |
| `country` | Country, when the ATS gives one |
| `isRemote`, `workplaceType` | `true` for remote jobs. `workplaceType` is `remote`, `hybrid`, `onsite` or empty |
| `employmentType` | `Full-time`, `Part-time`, `Contract`, `Internship`, `Temporary`, or the company's own label |
| `postedAt`, `updatedAt` | ISO dates in UTC. `updatedAt` only on Greenhouse and Recruitee |
| `url`, `applyUrl` | The job page and the application form |
| `salary` | `{ min, max, currency, period, text }` when the company publishes pay, otherwise empty |
| `salaryZones` | Greenhouse only, on jobs with pay. Every pay zone the company lists, with its own title, range, currency and period |
| `descriptionText`, `descriptionHtml` | The full job description as clean text and as the original HTML |
| `seniority` | `intern`, `entry`, `mid`, `senior`, `lead`, `executive`, or empty when the job gives no clear sign. Always the last field, see [Seniority filter](#seniority-filter) |

### Input

```json
{
  "companies": [
    "stripe.com",
    "Scale AI",
    "https://job-boards.greenhouse.io/discord",
    "https://jobs.lever.co/spotify",
    "https://www.notion.so/careers",
    "ramp",
    "workable:skroutz",
    "https://careers.smartrecruiters.com/Wix2"
  ],
  "keywords": ["engineer", "data"],
  "locations": ["London", "remote"],
  "postedWithinDays": 30
}
```

Each line in `companies` can be any of these.

| You pass | Example |
|---|---|
| A company website | `stripe.com`, `launchdarkly.com`, `nubank.com.br` |
| A company name | `Scale AI`, `Hims & Hers`, `Lucid Software` |
| A job board URL | `boards.greenhouse.io/figma`, `jobs.lever.co/palantir`, `jobs.ashbyhq.com/openai`, `apply.workable.com/huggingface`, `careers.smartrecruiters.com/Canva`, `bunq.recruitee.com` |
| Any job URL on a board | `jobs.lever.co/spotify/0eebb462-...` reads the whole Spotify board |
| A company career page | `notion.so/careers`, `figma.com/careers`, `palantir.com/careers` |
| A board name | `stripe`. The scraper checks every ATS for a board with that name, see below |
| A board name with the ATS | `greenhouse:stripe`, `lever:zoox`, `smartrecruiters:ServiceNow`. Faster and never ambiguous |

Board URLs and `ats:name` pairs are read exactly as given.
Everything else goes through the steps in the next section.

### Company websites and names

For a website or career page URL the scraper tries, in order:

1. Links to a supported board on the company's own pages: the page you gave, its careers links, and the usual `/careers`, `/jobs`, `careers.` and `jobs.` addresses. A page that links many companies' boards, like a job list, counts only for a board with the company's name.
2. A board named after the domain on each of the six systems, kept only when the company name the ATS reports matches (`stripe.com` and "Stripe"). When two such boards exist, the one whose jobs mention the domain wins.
3. The name the site gives itself, when it differs from the domain (`wikimediafoundation.org` calls itself "Wikimedia Foundation", whose board is `wikimedia`).
4. Rarer board names: `doordashusa` on Greenhouse, `Wix2` on SmartRecruiters.

For a company name it tries spellings of the name on each system (`scaleai`, `scale-ai`, `scale`), then the same page search on `<name>.com`, then the rarer board names.
A board counts only when the company name the ATS reports matches yours, ignoring case, spaces and words like Inc, Ltd or Labs.

A bare word such as `stripe` is first read as a board name.
A board whose company has that name wins.
Otherwise the first board with jobs in the order Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee is used, as before, even when its company has another name, so check `company` on the rows.

When several boards match, the one with at least twice the jobs of the next is used.
When two are closer than that, you get a free `ambiguous` row that lists up to 5 of them with their board URLs, and no jobs, so you can pass the one you want.
Workable, SmartRecruiters and Recruitee have free accounts, and many sit dormant under big names, such as a SmartRecruiters board "Apple2" whose one job dates from 2016.
A board on those three whose newest job is over a year old counts as empty.

What happened, measured on 238 real companies in three runs, given by website and by name, after the last change to how boards are found:

| Run | Companies | Jobs returned | Right board | Wrong board | Board exists, not found | Board exists, no open jobs | Unsupported system | Could not tell |
|---|---|---|---|---|---|---|---|---|
| `2xhrfhszjWElHXgtD` | 87 | 57 | 57 | 0 | 4, one of them `ambiguous` | 1 | 22 | 3 |
| `IbqbOw4k0nf9ZiwQM` | 79 | 50 | 50 | 0 | 2 | 0 | 14 | 13 |
| `emHhoiwCAXiPXAJfF` | 72 | 38 | 37 | 1 | 6 | 0 | 14 | 14 |

Of the 157 companies whose board on one of the six systems we confirmed, the scraper found the right one for 144 (92%), and 93%, 96% and 84% per run.
"Unsupported system" counts companies that hire through Workday (16), Phenom (9), iCIMS (5), Eightfold (4), SuccessFactors (3) and others.
The scraper named the system in a free `unsupported_ats` row for 30 of those 50.
For the other 20 the system shows only after the page's JavaScript runs, or the company runs its own job site.
One more `unsupported_ats` row was wrong: remote.com's job list links another company's Personio board.
"Could not tell" counts companies where we found no board on any system by hand either.
The misses were boards only JavaScript reveals (`digitalocean98` and `ada18` on Greenhouse, an Ashby board named by a UUID), companies renamed or acquired (Superhuman, Auth0, Weights & Biases), boards named differently from the company (`oscar` for Oscar Health, `montecarlodata` for Monte Carlo), and one board named `careers`.
The wrong board was `Hetzner`, a bare word, which matched a Recruitee board named `hetzner` that belongs to another company, shown as `company: xneelo`.

### Filters

| Filter | What it does |
|---|---|
| `keywords` | Keep jobs whose title, department or team contains any of these |
| `excludeKeywords` | Drop jobs whose title contains any of these, for example `senior` or `intern` |
| `locations` | Keep jobs whose location or country contains any of these. `remote` matches every remote job |
| `remoteOnly` | Keep only remote jobs |
| `postedWithinDays` | Keep only jobs posted in the last N days |
| `hasSalary`, `minSalary` | Keep only jobs that list pay, or pay that reaches a minimum, see [Salary filter](#salary-filter) |
| `seniority` | Keep only jobs at these levels, such as `intern` and `entry`, see [Seniority filter](#seniority-filter) |
| `onlyNewSinceLastRun` | Keep only jobs this scraper has not returned to you before, see below |
| `maxJobsPerCompany` | Stop after this many matching jobs per board |
| `includeDescription` | Turn off for a smaller dataset. SmartRecruiters boards also run faster without it |

Filters run before charging, so jobs they drop cost nothing.

### Salary filter

Both salary filters are off by default and read the `salary` the board itself sends.

| Input | What it does |
|---|---|
| `hasSalary` | Keep only jobs that list a pay amount |
| `minSalary` | Keep jobs whose listed pay reaches this amount. The top of the range counts, so a job paying 120,000 to 160,000 passes a minimum of 150,000 |
| `salaryCurrency` | The currency of `minSalary`, `USD` unless you set it |
| `salaryPeriod` | `YEAR`, `MONTH`, `WEEK`, `DAY` or `HOUR`, `YEAR` unless you set it |
| `keepJobsWithoutSalary` | With `minSalary`, also keep jobs that list no pay in that currency and period |

Amounts are never converted.
With `minSalary` of 150,000 USD per year, a job paid in CAD or GBP, or per hour, counts as having no salary.
Jobs with no salary are dropped, unless you turn on `keepJobsWithoutSalary`.
A board where no job passes gives a free `no_jobs` row that says so.

How much pay the boards list, measured on 61 real boards with 7,935 open jobs (run `D84UCSK0PMuLLr1gX`, no filter):

| System | Jobs | List a pay amount |
|---|---|---|
| Ashby | 1,956 | 1,203 |
| Lever | 1,012 | 307 |
| SmartRecruiters | 3,158 | 423 |
| Greenhouse | 1,600 | 919 |
| Workable | 187 | 0 |
| Recruitee | 22 | 0 |

Workable boards send no pay, so the salary filter drops all of their jobs.
Many companies write pay into the description instead, such as Notion on Ashby and Brex on Greenhouse, and the filter does not read descriptions.

#### Greenhouse pay zones

Greenhouse lists pay per zone, such as one range for New York and a lower one for Denver.
`salary` spans every zone in the first zone's currency, from the lowest minimum to the highest maximum, and `text` holds the zone titles.
`salaryZones` lists every zone as Greenhouse sends it.
Greenhouse has no period field, so `period` is set only when the zone titles name exactly one period, as in "Annual Base Salary" or "Base Pay Range (hourly)".
Otherwise it is empty, and `minSalary` treats the job as having no salary, because its period is unknown.
The text around a range is not read for this, because on 88 boards its period words were mostly about bonuses and stock grants.

Measured on 88 Greenhouse boards with 10,225 open jobs (run `qulMDQ0C9d9IkHB0b`), 2,435 jobs on 36 boards list pay.
Their titles name the year for 449, the hour for 136 and the month for 1, and no period for 1,849.
So `hasSalary` kept 2,435 jobs (run `nUOBNbtUFCxljij4o`), and `minSalary: 150000` kept 348 (run `8xcK72NJIR43cpfNg`).
We checked 20 of those jobs against their Greenhouse job pages by hand, and every zone range matched the page.
A few companies label a range wrongly, such as an "Annual Base Salary" of $35 to $58, and the rows keep what the company sent.

This is real output from run `qulMDQ0C9d9IkHB0b`, shortened.

```json
{
  "status": "ok",
  "input": "greenhouse:robinhood",
  "company": "Robinhood",
  "ats": "greenhouse",
  "boardSlug": "robinhood",
  "jobId": "6669758",
  "title": "Android Engineer, Government Products",
  "department": "ENG Infrastructure",
  "team": null,
  "location": "New York, NY",
  "locations": [
    "New York, NY"
  ],
  "country": null,
  "isRemote": false,
  "workplaceType": null,
  "employmentType": null,
  "postedAt": "2025-05-08T18:52:01Z",
  "updatedAt": "2026-10-02T22:48:15Z",
  "url": "https://boards.greenhouse.io/robinhood/jobs/6669758?t=gh_src=&gh_jid=6669758",
  "applyUrl": "https://boards.greenhouse.io/robinhood/jobs/6669758?t=gh_src=&gh_jid=6669758",
  "salary": {
    "min": 130000,
    "max": 195000,
    "currency": "USD",
    "period": null,
    "text": "Zone 1 (Menlo Park, CA; New York, NY; Bellevue, WA; Washington, DC); Zone 2 (Denver, CO; Westlake, TX; Chicago, IL); Zone 3 (Lake Mary, FL; Clearwater, FL; Gainesville, FL)"
  },
  "salaryZones": [
    {
      "title": "Zone 1 (Menlo Park, CA; New York, NY; Bellevue, WA; Washington, DC)",
      "min": 166000,
      "max": 195000,
      "currency": "USD",
      "period": null
    },
    {
      "title": "Zone 2 (Denver, CO; Westlake, TX; Chicago, IL)",
      "min": 147000,
      "max": 172000,
      "currency": "USD",
      "period": null
    },
    {
      "title": "Zone 3 (Lake Mary, FL; Clearwater, FL; Gainesville, FL)",
      "min": 130000,
      "max": 152000,
      "currency": "USD",
      "period": null
    }
  ],
  "descriptionText": "Join us in building the future of finance.\n\nOur mission is to democratize finance for all. An estima...",
  "descriptionHtml": "<div class=\"content-intro\"><h2>Join us in building the future of finance.</h2>\n<p>Our mission is to ..."
}
```

On SmartRecruiters, pay comes from the same extra request per job as the description, so a salary filter makes those boards slower even with `includeDescription` off.

With `minSalary: 150000` (run `DWdnqZebGdQH2Hb3J`) the scraper kept 1,763 jobs from 23 boards, every one paid in USD per year with a range that reaches 150,000.
Before Greenhouse pay was added, we checked 20 kept and 20 dropped jobs on the other systems against their job pages by hand, and all 20 kept jobs showed the same range on their page.
Turning the filters off gives the rows the scraper gave before they existed, except that Greenhouse jobs with pay now carry it.

This is real output from run `khMgPJqa0fyXPNgfv` on Ashby, shortened.
Its range starts below 150,000 and reaches above it, so it passes.

```json
{
  "status": "ok",
  "input": "ramp",
  "company": "Ramp",
  "ats": "ashby",
  "boardSlug": "ramp",
  "jobId": "4e64ab86-4e30-403b-b1b9-41dc052570ce",
  "title": "Software Engineer, Frontend",
  "department": "Engineering",
  "team": "Frontend",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "San Francisco, CA", "Remote (US)", "Miami, FL"],
  "country": "USA",
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "postedAt": "2023-03-09T17:44:00Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ramp/4e64ab86-4e30-403b-b1b9-41dc052570ce",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/4e64ab86-4e30-403b-b1b9-41dc052570ce/application",
  "salary": { "min": 143200, "max": 284000, "currency": "USD", "period": "YEAR", "text": "$143.2K - $284K" },
  "descriptionText": "About Ramp\n\nRamp is building the smart infrastructure for finance teams, embedded in the transaction flow of every dolla...",
  "descriptionHtml": "<h1><strong>About Ramp</strong></h1><p style=\"min-height:1.5em\">Ramp is building the smart infrastructure for ...",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "resolvedBy": "board_name",
  "resolvedFrom": null
}
```

A Workable board gave this free row in run `DWdnqZebGdQH2Hb3J`.

```json
{
  "status": "no_jobs",
  "input": "https://apply.workable.com/huggingface/",
  "ats": "workable",
  "boardSlug": "huggingface",
  "message": "Board has 6 matching jobs, none with a salary that passes your salary filter. This scraper reads no salary from Workable boards, so none of their jobs lists one"
}
```

With `onlyNewSinceLastRun`, the salary filter is part of the filter set, so turning it on starts a new memory for that board.
Runs without it keep the memory they had.

### Seniority filter

Every job row ends with `seniority`, one of `intern`, `entry`, `mid`, `senior`, `lead` or `executive`, or empty when the job gives no clear sign.
The `seniority` filter keeps only the levels you list, and it is off by default.

| Input | What it does |
|---|---|
| `seniority` | Keep jobs at these levels. `["intern", "entry"]` finds internships, new grad and junior roles |
| `keepJobsWithoutSeniority` | With `seniority` set, also keep jobs whose level is empty |

The scraper reads the level in this order, and the first answer wins.

| Step | What it reads | Examples |
|---|---|---|
| 1 | Level words in the title | Intern, Co-op, Working Student, Stage, New Grad, Graduate, Junior, Entry Level, Senior, Sr., Staff, Principal, Lead, Supervisor, Engineering Manager, Director, VP, Head of, Chief |
| 2 | The board saying intern or early career | An `Internship` employment type, an Internships, Early Career or University department |
| 3 | The years of experience the description asks for | 0 or 1 year is `entry`, 2 to 4 is `mid`, 5 or more is `senior`. Also "this internship" (intern) and "new grad" or "recent graduates" (entry) |
| 4 | A level number or "Associate" in the title | Engineer I is `entry`, II is `mid`, III is `senior`. Sales Associate is `entry` |
| 5 | The board's own level field | SmartRecruiters and Workable experience level, Recruitee experience, Lever level |

`lead` covers staff and principal engineers and people managers.
`executive` covers directors and above.
Titles that only look like levels are skipped, such as Account Executive, Member of Technical Staff, Staff Accountant and University Recruiter.
A title with a range, such as "Senior / Staff" or "Junior - Mid level", gets the lowest level in it.
The board's level field comes last because companies set it loosely: on 8 SmartRecruiters boards, 19 of the 36 "Entry Level" jobs whose title names a level had "Senior" in the title.

Jobs with an empty level are dropped by the filter, unless you turn on `keepJobsWithoutSeniority`.
On 61 real boards with 7,934 open jobs (run `P6vp0RLRT5mbKdPoL`, no filter), 1,308 jobs (16%) had no clear level.
Only 772 were intern or entry, so keeping the empty ones would make most rows of an intern search jobs with no level.

| Level | Jobs |
|---|---|
| intern | 317 |
| entry | 455 |
| mid | 1,487 |
| senior | 2,458 |
| lead | 1,506 |
| executive | 403 |
| empty | 1,308 |

With `seniority: ["intern", "entry"]` on 75 real boards across all six systems (run `yeKF2Sv0WnKDsVXem`), every board was read with no error.
The scraper kept 1,927 jobs (444 intern, 1,483 entry) from 58 boards, and the other 17 boards each gave a free `no_jobs` row.
A hand check of 30 kept jobs from 18 boards against their job pages found 28 at the level given and 29 that are intern or entry jobs.
One was a fixed-term program for current university students, labelled `entry` where `intern` fits better. The other was a yoga instructor role asking for teaching experience, which only the board's own level field called entry.

On SmartRecruiters, the description comes from one extra request per job, so the seniority filter makes those boards slower even with `includeDescription` off.
With the filter off and `includeDescription` off, SmartRecruiters rows get their level without the description.
With `onlyNewSinceLastRun`, the seniority filter is part of the filter set, so turning it on starts a new memory for that board.
Runs without it keep the memory they had.

### Monitoring: only new jobs

Turn on `onlyNewSinceLastRun` and schedule the run, for example daily.
The first run returns every matching job and remembers them.
Each later run returns only jobs it has not returned to you before for the same board and filters, so you pay only for new postings.
A job that a company re-posts under a new ID counts as new.
A board with nothing new comes back as a free `no_jobs` row.
The memory is a key-value store named `career-site-jobs-scraper-monitor` in your own Apify account, one record per board and filter set.
Delete that store to start over.

### Output example

This is real output for a Lever board URL from run `P6vp0RLRT5mbKdPoL`, with the description shortened here.

```json
{
  "status": "ok",
  "input": "https://jobs.lever.co/aircall",
  "company": "aircall",
  "ats": "lever",
  "boardSlug": "aircall",
  "jobId": "0fbc9b05-520b-4cbb-a276-5a9a75e8f3bf",
  "title": "Regional Marketing Intern - NA Market",
  "department": "Marketing",
  "team": "11003 - Demand Generation",
  "location": "San Francisco Office",
  "locations": ["San Francisco Office"],
  "country": "US",
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": "Internship",
  "postedAt": "2026-01-14T18:14:31Z",
  "updatedAt": null,
  "url": "https://jobs.lever.co/aircall/0fbc9b05-520b-4cbb-a276-5a9a75e8f3bf",
  "applyUrl": "https://jobs.lever.co/aircall/0fbc9b05-520b-4cbb-a276-5a9a75e8f3bf/apply",
  "salary": { "min": 20, "max": 22, "currency": "USD", "period": "HOUR", "text": "The hourly rate is not including benefits, including health insurance and public transit + gym reimbursement." },
  "descriptionText": "Aircall is a unicorn, AI-powered customer communications platform used by 22,000+ companies worldwide to drive revenue, resolve issues faste...",
  "descriptionHtml": "<div><span style=\"font-size: 16px;\">Aircall is a unicorn, AI-powered customer communications platform used by 22,000+ co...",
  "seniority": "intern"
}
```

This is real output for the board name `ramp` from the same run, shortened the same way.
`boardUrl`, `resolvedBy` and `resolvedFrom` say which board the scraper used and how it found it.

```json
{
  "status": "ok",
  "input": "ramp",
  "company": "Ramp",
  "ats": "ashby",
  "boardSlug": "ramp",
  "jobId": "fc971889-db1d-4a20-a25e-f282f9296936",
  "title": "University Grad | Customer Experience Associate",
  "department": "Customer Experience",
  "team": "Customer Experience",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "San Francisco, CA", "Remote (US)"],
  "country": "USA",
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "postedAt": "2026-09-09T12:19:47Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ramp/fc971889-db1d-4a20-a25e-f282f9296936",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/fc971889-db1d-4a20-a25e-f282f9296936/application",
  "salary": { "min": 60000, "max": 70000, "currency": "USD", "period": "YEAR", "text": "$60K - $70K" },
  "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": "<h1><strong>About Ramp</strong></h1><p style=\"min-height:1.5em\">Ramp is building the smart infrastructure for finance te...",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "resolvedBy": "board_name",
  "resolvedFrom": null,
  "seniority": "entry"
}
```

`resolvedBy` is `careers_page_link` when a company page linked the board (`resolvedFrom` is that page), `company_name_match` when the board's company name matched, or `board_name` when a bare word matched a board name.

A company on a system this scraper does not read gives one free row, from an earlier test run.

```json
{
  "status": "unsupported_ats",
  "input": "Nike",
  "ats": "workday",
  "boardSlug": null,
  "message": "'Nike' uses workday, linked as https://nike.wd1.myworkdayjobs.com. This actor reads Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee boards only.",
  "boardUrl": "https://nike.wd1.myworkdayjobs.com"
}
```

Two close boards give one free `ambiguous` row, from run `2xhrfhszjWElHXgtD`.

```json
{
  "status": "ambiguous",
  "input": "Doctolib",
  "ats": null,
  "boardSlug": null,
  "message": "Several boards match 'Doctolib' about equally well (none has twice the jobs of the next): Doctolib on Greenhouse, 149 jobs, https://job-boards.greenhouse.io/Doctolib; Doctolib on Ashby, 146 jobs, https://jobs.ashbyhq.com/Doctolib. Pass the board URL you want.",
  "candidates": [
    { "ats": "greenhouse", "boardSlug": "Doctolib", "company": "Doctolib", "jobs": 149, "boardUrl": "https://job-boards.greenhouse.io/Doctolib" },
    { "ats": "ashby", "boardSlug": "Doctolib", "company": "Doctolib", "jobs": 146, "boardUrl": "https://jobs.ashbyhq.com/Doctolib" }
  ]
}
```

A board that doesn't exist gives one free row too.

```json
{
  "status": "not_found",
  "input": "greenhouse:thisdoesnotexist999",
  "ats": "greenhouse",
  "boardSlug": "thisdoesnotexist999",
  "message": "Board does not exist (https://boards-api.greenhouse.io/v1/boards/thisdoesnotexist999/jobs answered 404)"
}
```

| Status | Meaning | Charged |
|---|---|---|
| `ok` | One open job | Yes |
| `no_jobs` | The board exists but has no open jobs, or none matched your filters, including the salary and seniority filters | No |
| `not_found` | No such board on that ATS | No |
| `ats_not_detected` | No supported board found for the website, name or page. The message lists the pages read and the board names tried | No |
| `unsupported_ats` | The company's pages link to a system this scraper does not read, such as Workday, iCIMS or SuccessFactors. `ats` names it and `boardUrl` is the link found | No |
| `ambiguous` | Several boards match the name about equally well. `candidates` lists them with their board URLs | No |
| `invalid_input` | The line is not a URL, a name or a board name | No |
| `error` | The ATS kept failing after retries | No |

### Use it from AI agents

The scraper works as a tool through the [Apify MCP server](https://mcp.apify.com) in Claude, Cursor, VS Code and other MCP clients.
An agent can pass a list of companies and a keyword, and get back structured jobs it can rank, summarise or match to a CV.

You can also call it through the [Apify API](https://docs.apify.com/api/v2) or the Python and JavaScript clients, and schedule it or connect it to Make, Zapier and n8n.
A daily schedule with `onlyNewSinceLastRun: true` gives you a feed of new jobs at the companies you track.

### Pricing

Prices are per 1,000 jobs.

| Event | No discount | Bronze | Silver | Gold |
|---|---|---|---|---|
| Job returned | $1.50 | $1.35 | $1.10 | $0.90 |
| Board not found, empty, filtered out, undetected, unsupported, ambiguous or failed | Free | Free | Free | Free |

Bronze, Silver and Gold are Apify Store discount tiers, and Apify's Pricing tab shows them as Starter, Scale and Business.
Apify decides which tier an account is on.

Set a maximum cost per run in the run options.
The scraper stops once that limit is reached.

### FAQ

**How do I find a company's board name?**
You usually don't need to: pass the company's website or name and check `boardUrl` on the rows.
To be certain, open its careers page and click any job.
The address shows the ATS and the board name, for example `jobs.lever.co/zoox/...` means Lever, board `zoox`.
A board URL is always read exactly as given.

**Why is a field empty for some jobs?**
Each ATS publishes different fields, and companies fill in different ones.
Greenhouse has no employment type field of its own, so it only appears when the company adds one.
Salary appears only when the company publishes it in the board's own pay fields.
Workable boards send no pay, so their `salary` is always empty, even when the description states pay.

**How fresh is the data?**
Every run reads the live board, so you get the jobs open at that moment.

**Is a job listed once per location?**
No.
A job with several locations is one row, with all of them in `locations`.

### Limitations

- Only Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee boards. Workday, iCIMS, Taleo and custom career sites are not supported.
- It reads the boards you give it, or the ones it finds for the companies you name. It does not search across all companies.
- Finding a board from a website or name reads only the HTML the site sends. Boards that only JavaScript reveals, boards with numbered names such as `digitalocean98`, and companies renamed or acquired are often missed. Pass the board URL for those.
- Workable boards give a posting date without a time.
- SmartRecruiters needs one extra request per job for the description, so large SmartRecruiters boards take longer.
- `seniority` is worked out by fixed rules from the title, the description and the board's level fields, not read from a field every board has. On the 30 hand-checked jobs it was right for 28, and 16% of jobs get no level.
- The salary filter reads only the board's pay fields. It never converts currencies or periods, it drops every Workable job, and Greenhouse pay whose zone titles name no period never passes `minSalary`.

# Actor input Schema

## `companies` (type: `array`):

One per line. A company website (stripe.com), a company name (Scale AI), a job board URL (boards.greenhouse.io/stripe, jobs.lever.co/spotify, jobs.ashbyhq.com/openai, apply.workable.com/huggingface, careers.smartrecruiters.com/Canva, bunq.recruitee.com), a career page URL (notion.so/careers), a board name (stripe) or a board name with the ATS (greenhouse:stripe). For websites and names the scraper finds the board itself, and each row says which board it used. Board URLs and ATS:name pairs are read exactly as given.

## `keywords` (type: `array`):

Keep jobs whose title, department or team contains any of these words or phrases. Case does not matter. Leave empty for all jobs.

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these, for example senior or intern.

## `locations` (type: `array`):

Keep jobs whose location or country contains any of these, for example London, Germany or US. The word remote matches every remote job.

## `remoteOnly` (type: `boolean`):

Keep only jobs the ATS marks as remote, or whose location says remote.

## `postedWithinDays` (type: `integer`):

Keep only jobs posted in the last N days. 0 keeps all.

## `hasSalary` (type: `boolean`):

Keep only jobs whose board lists a pay amount. Workable boards send no pay, so this drops all of their jobs.

## `minSalary` (type: `integer`):

Keep jobs whose listed pay reaches this amount in the currency and period below. A job paying 120,000 to 160,000 passes a minimum of 150,000. Amounts are never converted between currencies or periods. 0 turns it off.

## `salaryCurrency` (type: `string`):

Currency of the minimum salary, as a 3-letter code such as USD, EUR or GBP. Jobs paid in another currency count as having no salary.

## `salaryPeriod` (type: `string`):

The period the minimum salary is paid per. Jobs paid per another period count as having no salary, and so does Greenhouse pay whose zone titles name no period.

## `keepJobsWithoutSalary` (type: `boolean`):

With a minimum salary set, also keep jobs that list no pay in that currency and period. Off by default, so those jobs are dropped.

## `seniority` (type: `array`):

Keep only jobs at these levels, for example intern and entry for students and new grads. Every job row has a seniority field, read from the title, the years of experience the description asks for, and the level fields the board sends. Leave empty for all jobs.

## `keepJobsWithoutSeniority` (type: `boolean`):

With a seniority filter set, also keep jobs whose level the title, description and board do not show (seniority null). Off by default, so those jobs are dropped.

## `onlyNewSinceLastRun` (type: `boolean`):

For scheduled runs. Returns only jobs this actor has not returned to you before for the same board and filters. The first run returns every matching job and remembers them. The memory is a key-value store named career-site-jobs-scraper-monitor in your Apify account.

## `maxJobsPerCompany` (type: `integer`):

Stop after this many matching jobs per board. 0 means no limit.

## `includeDescription` (type: `boolean`):

Adds the full description as plain text and HTML. Turn off for a smaller dataset. SmartRecruiters boards also run faster without it.

## `maxConcurrency` (type: `integer`):

How many company boards to read at the same time.

## `proxyConfiguration` (type: `object`):

Not needed. These job feeds are public and answer without a proxy. Turn it on only if a board starts refusing requests.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/discord",
    "https://jobs.lever.co/outreach",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/huggingface/",
    "https://careers.smartrecruiters.com/Wix2"
  ],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "hasSalary": false,
  "minSalary": 0,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "keepJobsWithoutSalary": false,
  "keepJobsWithoutSeniority": false,
  "onlyNewSinceLastRun": false,
  "maxJobsPerCompany": 0,
  "includeDescription": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

One item per open job with company, ATS, title, department, locations, remote flag, employment type, dates, job and apply URLs, salary when published, and the full description. Boards that fail or have no jobs get one uncharged item with a status and message. Only items with status ok are charged.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "https://job-boards.greenhouse.io/discord",
        "https://jobs.lever.co/outreach",
        "https://jobs.ashbyhq.com/linear",
        "https://apply.workable.com/huggingface/",
        "https://careers.smartrecruiters.com/Wix2"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("heyitsgautham/career-site-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "companies": [
        "https://job-boards.greenhouse.io/discord",
        "https://jobs.lever.co/outreach",
        "https://jobs.ashbyhq.com/linear",
        "https://apply.workable.com/huggingface/",
        "https://careers.smartrecruiters.com/Wix2",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("heyitsgautham/career-site-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "https://job-boards.greenhouse.io/discord",
    "https://jobs.lever.co/outreach",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/huggingface/",
    "https://careers.smartrecruiters.com/Wix2"
  ]
}' |
apify call heyitsgautham/career-site-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,heyitsgautham/career-site-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/v1wraWtOcNb8IFh26/builds/dhHUrf9ujI3XzyW5b/openapi.json
