# ATS Job Feed: Greenhouse, Lever, Ashby & More (`saiberops/ats-job-feed`) Actor

Open jobs from company career boards on Greenhouse, Lever, Ashby, Recruitee and Personio in one schema: title, team, location, remote, employment type, pay range, posted date, apply URL. Paste board or careers-page URLs.

- **URL**: https://apify.com/saiberops/ats-job-feed.md
- **Developed by:** [Saiber Operations](https://apify.com/saiberops) (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 $1.20 / 1,000 job returneds

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

## ATS Job Feed: Greenhouse, Lever, Ashby, Recruitee & Personio Jobs in One Schema

Get every open job from a list of companies as clean, identical rows, no matter which applicant-tracking system (ATS) runs their careers page. Paste job-board URLs, `ats:token` shorthands, or plain careers-page URLs, and the Actor finds the board, reads the employer's own public job-board feed, and returns one normalized row per job: title, department, team, locations with city / region / country, remote or hybrid, employment type, **structured pay range**, posted date and apply link.

**Built for:**

- **B2B sales and RevOps.** Hiring is an early buying signal: a company opening three SDR roles is about to buy sales tooling. Pull your target accounts' open roles every week.
- **Recruiters and sourcers.** One feed across Greenhouse, Lever, Ashby, Recruitee and Personio instead of five scrapers.
- **Job boards and aggregators.** Live, deduplicated feeds with a stable `globalId` per job.
- **VC, PE and market analysts.** Track hiring at portfolio companies and competitors by team, location and pay.
- **AI agents.** One call, structured JSON, no API key.

**What makes it different:** one schema across ATSs (same field names, same enums), pay ranges parsed into numbers where the employer publishes them, careers-page auto-detection so you can paste a list of company sites, no minimum per run, and no recruiter or hiring-manager names or emails, ever.

### Sample output

```json
{
  "globalId": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "ats": "ashby",
  "company": "ramp",
  "companyName": "Ramp",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "requisitionId": null,
  "title": "Security Engineer, Cloud",
  "departments": ["Engineering"],
  "teams": ["Backend"],
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "primaryLocation": "New York, NY (HQ)",
  "city": "New York City",
  "region": "NY",
  "country": "US",
  "workplaceType": "hybrid",
  "workplaceTypeSource": "ats",
  "isRemote": false,
  "employmentType": "full_time",
  "employmentTypeRaw": "FullTime",
  "experienceLevel": null,
  "compensation": {
    "min": 211400,
    "max": 290600,
    "currency": "USD",
    "interval": "year",
    "summary": "$211.4K – $290.6K • Offers Equity",
    "source": "ashby_compensation",
    "offersEquity": true
  },
  "postedAt": "2026-04-07T17:12:35Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "isListed": true,
  "language": null,
  "contentHash": "5f0c0e3d4a8b9f6e2c1d7a3b8e9f0a1b2c3d4e5f",
  "input": "https://jobs.ashbyhq.com/ramp",
  "fetchedAt": "2026-10-24T12:00:00Z"
}
```

(Illustrative example.) Dataset tabs: **Overview**, **Pay ranges**, **Locations and workplace**, **Departments and teams**. Two extra records are saved with every run: `DETECTION` (how each input line was resolved, or why it wasn't) and `RUN_SUMMARY` (jobs seen, matched and returned per company, plus any errors).

### Inputs

1. Put one company per line in **Companies**. Any of these work:
   - a job-board URL: `https://boards.greenhouse.io/coinbase`, `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/ramp`, `https://acme.recruitee.com`, `https://acme.jobs.personio.de`
   - a shorthand: `greenhouse:stripe`, `lever:spotify`, `lever:eu/acme`, `ashby:ramp`, `recruitee:acme`, `personio:acme`
   - a careers-page URL such as `https://www.example.com/careers`: the Actor reads that page once and looks for the board it links to or embeds.
2. Optionally narrow the results with filters, then click **Start**.

| Input | What it does |
|---|---|
| Companies | Board URLs, shorthands or careers pages. Up to 1,000 per run. If the field is left out of the input entirely (e.g. an API call without it), the three example boards are read; an empty list does nothing. |
| Max jobs per company / per run | Caps on returned (charged) jobs. The run stops cleanly at the cap. |
| Title contains any of / none of | Case-insensitive title filters. |
| Department or team contains any of | Matches departments and teams. |
| Location contains any of, Countries | Location text, city, region, or ISO country code. |
| Remote or hybrid only | Keeps remote and hybrid roles. |
| Employment types | Full-time, part-time, contract, internship, temporary. |
| Only jobs with a structured pay range | Keeps jobs with numeric pay published by the employer. |
| Posted within the last N days | Uses the posted date; jobs without one are kept. |
| Detect the ATS from careers pages | On by default. Reads unrecognised URLs once, respecting robots.txt. |
| Guess the board name if detection finds nothing | Off by default. Tries `acme` from `acme.com` on Greenhouse, Lever and Ashby. |
| Include unlisted Ashby jobs | Off by default (the employer asked for direct-link-only access). |
| Respect robots.txt on careers pages | On by default; turn off only for pages you own. |

Plain company names without a URL aren't accepted: paste the careers-page URL instead, so the Actor never has to guess which "Acme" you mean.

### Pricing

Pay per event: you pay only for jobs returned and for careers pages successfully detected.

| Event | Free plan | Bronze | Silver | Gold |
|---|---|---|---|---|
| Job returned | $0.0018 | $0.0015 | $0.00135 | $0.0012 |
| Careers page detected | $0.0025 | $0.002 | $0.0018 | $0.0016 |

That's $1.20–$1.80 per 1,000 jobs, plus Apify's standard Actor start fee. **Not charged:** jobs removed by your filters, unlisted jobs you didn't ask for, companies whose board can't be read, board URLs and shorthands (no detection needed), detections served from the 30-day detection cache, and detections whose board then doesn't answer. There is no minimum per run. Set **Max cost per run** to cap spending; the Actor stops cleanly at the cap.

### Integrations and API

Schedule it in Apify (for example weekly over your account list) and send rows to Google Sheets, a CRM, Slack, Make, Zapier or n8n through Apify's integrations, or call it from the Apify API. AI agents can use it through Apify's MCP server: give it a list of companies, get back their open roles. Use `globalId` to deduplicate across runs and `contentHash` to spot edited postings.

### Limits

- **Supported ATSs:** Greenhouse, Lever, Ashby, Recruitee and Personio. Workable, SmartRecruiters, BambooHR, Teamtailor, Rippling, Workday and iCIMS boards are recognised (the `DETECTION` record says so) but not read yet.
- **No job descriptions.** Description text often names recruiters or hiring managers and lists their email and phone, so this Actor does not output it. You get the job URL to read the full posting.
- **Field coverage depends on the ATS:**

| Field | Greenhouse | Lever | Ashby | Recruitee | Personio |
|---|---|---|---|---|---|
| Departments / teams | departments | both | both | department | department, category |
| Workplace type | from location text | yes | yes | yes | from location text |
| Employment type | not published (`unknown`) | yes | yes | yes | yes |
| Pay range | when the employer publishes it (interval inferred) | when published | when published | when published | no |
| Posted date | yes | yes | yes | yes | yes |
| Updated date | yes | no | no | yes | no |

- City, region and country are taken from structured ATS fields where they exist and otherwise parsed from the location text with a conservative table; unknown parts are `null`, never guessed. A two-letter code that is both a US state and a country (`IL`, `IN`, `DE`, `CO`, `MA`…) is read from the city when the city is known ("Berlin, DE" is Germany, "Boston, MA" is Massachusetts) and otherwise left `null`; `CA` after a Canadian province is Canada.
- Greenhouse pay ranges have no interval field. The range title is read first ("Hourly", "Annual", "Monthly", "Weekly", "Daily"). Only when the title names none, and only in USD-scale currencies (USD, EUR, GBP, CAD, AUD, CHF, NZD, SGD), ranges up to 1,000 are labelled hourly and from 20,000 up yearly (`intervalInferred: true`); anything else has `interval: null`.
- Pay amounts are in the employer's currency; there is no currency conversion.
- Careers pages that load jobs with JavaScript from an unsupported ATS can't be detected. Paste the board URL instead.
- A host that answers 403 or 429 (or a bot challenge) is not called again during that run; the status message names it. No proxies are used.

### FAQ

**How fresh is the data?** Live: every run reads each board at that moment. Nothing comes from a pre-collected database.

**Why is a company missing from the results?** Open the `DETECTION` record. Common reasons: `NO_ATS_FOUND` (the page doesn't link a supported board), `NOT_SUPPORTED` (an ATS we recognise but don't read yet), `BOARD_NOT_FOUND` (wrong board name), `ROBOTS_DISALLOWED` (the careers page's robots.txt blocks automated access), `UNSAFE_TARGET` (the URL, or a redirect from it, points at a private or internal address; it is never contacted). Errors are never charged.

**A Lever company returns zero jobs.** Lever answers an empty list both for boards with no open roles and for names that don't exist, so the Actor can't tell them apart. Check the board URL in a browser. A careers page that points at an empty Lever board is not charged as a detection and not cached.

**How do I get only new jobs each week?** Schedule the Actor and deduplicate on `globalId` in your sheet or database.

**Do you return hiring-manager or recruiter contacts?** No, by design. Rows contain company, role, location, pay, dates and URLs only.

**How much memory does it need?** 512 MB (the default) covers boards with thousands of jobs.

### Responsible use and data sources

The Actor reads only what each employer already publishes for its own careers page, through the ATS vendors' public, no-login job-board endpoints: the Greenhouse Job Board API, the Lever Postings API, the Ashby public job posting API, the Recruitee Careers Site API and Personio's public job XML feed. It identifies itself as `SaiberOpsBot` with a contact address, follows robots.txt (RFC 9309) on every host it calls, paces itself to about two requests per second per host, never logs in, never uses proxies, and stops calling a host that answers 403 or 429. Output is business data about job openings; free-text fields that can contain personal data are never read into the output. This Actor is not affiliated with or endorsed by any ATS vendor or employer.

### Support

Found a bug, or need another ATS? Open an issue on this Actor's **Issues** tab and we'll respond there.

*Maintained by Saiber Operations LLC.*

# Changelog

This Actor's version history is a separate document: https://apify.com/saiberops/ats-job-feed/changelog.md

# Actor input Schema

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

One per line. Job-board URLs (boards.greenhouse.io/stripe, jobs.lever.co/spotify, jobs.ashbyhq.com/ramp, acme.recruitee.com, acme.jobs.personio.de), shorthands such as greenhouse:stripe or lever:eu/acme, or a company careers-page URL (the ATS is detected from the page). Up to 1,000 per run.

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

Counted after filters. Each returned job is one charged event.

## `maxJobs` (type: `integer`):

The run stops cleanly after this many jobs (or earlier, at your max cost per run).

## `titleIncludes` (type: `array`):

Case-insensitive words or phrases, e.g. sales, engineer, SDR. Leave empty for all titles.

## `titleExcludes` (type: `array`):

Case-insensitive, e.g. intern, senior.

## `departmentIncludes` (type: `array`):

Matched against departments and teams, e.g. Marketing, Sales, Security.

## `locationIncludes` (type: `array`):

Matched against every location string plus city, region and country, e.g. Atlanta, GA, London, Remote.

## `countries` (type: `array`):

Two-letter codes such as US, GB, DE. Jobs whose country can't be determined are kept.

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

Keep jobs whose workplace type is remote or hybrid.

## `employmentTypes` (type: `array`):

Leave empty for all. Greenhouse does not publish an employment type, so its jobs are 'unknown' and are dropped by this filter.

## `withPayOnly` (type: `boolean`):

Keep jobs where the employer published a numeric pay range.

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

0 means any date. Jobs without a posted date are kept.

## `autoDetect` (type: `boolean`):

For URLs that aren't job-board URLs, read the page once (respecting robots.txt) and look for an embedded or linked Greenhouse, Lever, Ashby, Recruitee or Personio board. A successful detection is charged once.

## `probeGuessedTokens` (type: `boolean`):

Try the domain name (acme for acme.com) as a board name on Greenhouse, Lever and Ashby. A hit is charged as a detection. Can match an unrelated company with the same name.

## `includeUnlisted` (type: `boolean`):

Ashby jobs the employer marked as reachable only by direct link. Off by default out of respect for that choice.

## `respectRobots` (type: `boolean`):

On by default. Turn off only for careers pages you own. Job-board API hosts always follow robots.txt.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/coinbase",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxJobsPerCompany": 50,
  "maxJobs": 150,
  "titleIncludes": [],
  "titleExcludes": [],
  "departmentIncludes": [],
  "locationIncludes": [],
  "countries": [],
  "remoteOnly": false,
  "employmentTypes": [],
  "withPayOnly": false,
  "postedWithinDays": 0,
  "autoDetect": true,
  "probeGuessedTokens": false,
  "includeUnlisted": false,
  "respectRobots": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `detection` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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://boards.greenhouse.io/coinbase",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "maxJobsPerCompany": 50,
    "maxJobs": 150,
    "titleIncludes": [],
    "titleExcludes": [],
    "departmentIncludes": [],
    "locationIncludes": [],
    "countries": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("saiberops/ats-job-feed").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://boards.greenhouse.io/coinbase",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
    ],
    "maxJobsPerCompany": 50,
    "maxJobs": 150,
    "titleIncludes": [],
    "titleExcludes": [],
    "departmentIncludes": [],
    "locationIncludes": [],
    "countries": [],
}

# Run the Actor and wait for it to finish
run = client.actor("saiberops/ats-job-feed").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://boards.greenhouse.io/coinbase",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxJobsPerCompany": 50,
  "maxJobs": 150,
  "titleIncludes": [],
  "titleExcludes": [],
  "departmentIncludes": [],
  "locationIncludes": [],
  "countries": []
}' |
apify call saiberops/ats-job-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,saiberops/ats-job-feed"
        }
    }
}
```

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/VTYt3wJ1XpvSTdfUz/builds/2T0jfWfdNVpHbBMGM/openapi.json
