# Multi-ATS Job Feed Aggregator (Greenhouse, Lever, Ashby) (`inn_corp/job-feed-aggregator`) Actor

Fetch open job postings from Greenhouse, Lever, and Ashby company career boards via their own public JSON APIs. One normalized record schema across all three platforms. No personal data, contact info in descriptions stripped by default.

- **URL**: https://apify.com/inn\_corp/job-feed-aggregator.md
- **Developed by:** [Inn Corp](https://apify.com/inn_corp) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job posting retrieveds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Multi-ATS Job Feed Aggregator

Give it board identifiers for Greenhouse, Lever, and Ashby and get back one
normalized record shape across all three, contact info stripped out of the
description text by default. Real demand exists for this kind of feed, and
the incumbents we checked were inconsistently rated, so the gap is not data
access, it is a normalized, trustworthy schema. This Actor: one schema
instead of three raw ones, honest nulls instead of guesses, and a courtesy
privacy default we haven't seen offered elsewhere in the category.

### What it does

- Give it board tokens/slugs/names for any mix of Greenhouse, Lever, and
  Ashby career boards.
- Get back one job record per posting, same field names regardless of which
  platform it came from: `platform`, `boardId`, `jobId`, `title`,
  `department`, `location`, `remote`, `employmentType`, `postedDate`, `url`,
  `description`, `applyUrl`, `fetchedAt`.
- Full job description text included by default (`includeDescription`),
  fetched in the same request this Actor already makes per board on all
  three platforms, so turning it off saves no requests, only output size.
- Emails and phone-number-shaped text found inside description text are
  regex-stripped by default (`stripContactInfo`), a courtesy default we
  haven't seen offered by comparable actors in this category.
- Deduped within each board by the platform's own job ID.
- A bad board identifier produces one clean warning; the run continues with
  every other board, and nothing is charged for the board that failed.

### What it deliberately does not do

- **No fourth platform faked.** Workable was evaluated for v1 and dropped.
  See "What we verified" below for the full evidence trail. Nothing pretends
  to support it.
- **No invented fields.** Every value comes straight from the platform's own
  response. The two fields that are lightly derived, not invented, are
  documented: `remote` (Ashby returns it directly as `isRemote`; Greenhouse
  and Lever do not expose a boolean at all, so it is set from whether the
  platform's own location/workplace text says "remote", and left `null` when
  there is nothing to check) and Greenhouse `employmentType` (Greenhouse's
  API has no standard field for it; if the company published a custom
  metadata field literally named "Employment Type" it is used, otherwise the
  field is `null`, never guessed).
- **No guarantee on contact-info stripping.** It is a regex courtesy, not a
  compliance control: common email and phone shapes are caught, unusual
  formats might not be. Read it as "we strip what we find," not "we
  guarantee none remains."
- **No candidate or applicant data, ever.** These endpoints cannot return it
  even if asked; see Ethics below.

### What we verified (2026-08-25)

Every platform below was hit live before any code was written, against real
company boards, not remembered from documentation:

- **Greenhouse** — `boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true`,
  no auth. Verified against `gitlab` (210 open roles) and `robinhood` (258).
  The `content=true` flag is a live-confirmed, official query parameter that
  returns each job's departments, offices, and full description HTML in the
  same list response — no per-job detail request needed.
- **Lever** — `api.lever.co/v0/postings/{site}?mode=json`, no auth. Verified
  against `leverdemo` (Lever's own public demo board) and `palantir` (308
  open roles).
- **Ashby** — `api.ashbyhq.com/posting-api/job-board/{name}`, no auth.
  Verified against `ramp` (139 open roles) and `notion`.
- **Workable — dropped.** The documented public endpoint
  (`apply.workable.com/api/v3/accounts/{account}/jobs`) returns HTTP 200
  with a well-formed but completely empty result (`{"total":0,"results":[]}`)
  for every one of 20+ real company accounts tested, including one (Automattic)
  pulled directly from a live Workable-hosted careers page — its account ID
  was read straight out of that page's own HTML, and the exact endpoint path
  (`/api/v3/accounts/{id}/jobs`) was confirmed by downloading and reading
  Workable's own careers-page JavaScript bundle. A companion
  `/jobs/filters` facet endpoint came back empty for all 20+ accounts too.
  Twenty-plus real companies all showing zero open roles at once is not a
  coincidence; it reads as the endpoint no longer serving data to
  non-browser callers, whatever the underlying reason. Rather than fake
  support for a platform that does not actually return data, v1 ships three
  platforms, honestly, and Workable will only be added back if a working
  keyless path is found.

### Ethics and data source

Every field comes from a company's own public job-board API: the same
keyless JSON endpoint each platform's "embed our openings on your careers
page" widget calls, built by Greenhouse, Lever, and Ashby for exactly this
kind of external consumption. This Actor returns job **postings**, not
people — none of these endpoints expose applicant or candidate data, so
there is nothing of that kind to fetch even by mistake. If you supply your
own company's board identifier, you are pulling your own listings; if you
supply a public company's, you are pulling what that company already
publishes to every visitor of its careers page and every other tool built on
the same API.

### Output examples

Real records from a live run, one per platform (`maxJobsPerBoard: 1`,
`includeDescription: true`), description trimmed for length:

```json
{
  "platform": "greenhouse",
  "boardId": "gitlab",
  "jobId": "8503792002",
  "title": "Account Executive - Italy",
  "department": "EMEA - Commercial",
  "location": "Remote, Italy",
  "remote": true,
  "employmentType": null,
  "postedDate": "2026-04-17",
  "url": "https://job-boards.greenhouse.io/gitlab/jobs/8503792002",
  "applyUrl": "https://job-boards.greenhouse.io/gitlab/jobs/8503792002",
  "description": "GitLab is the intelligent orchestration platform for DevSecOps...",
  "fetchedAt": "2026-08-25T03:34:28+00:00"
}
```

```json
{
  "platform": "lever",
  "boardId": "leverdemo",
  "jobId": "33538a2f-d27d-4a96-8f05-fa4b0e4d940e",
  "title": "AbelsonTaylor Writer",
  "department": "Professional Services",
  "location": "Arlington, TX",
  "remote": false,
  "employmentType": "Regular Full Time (Salary)",
  "postedDate": "2019-03-21",
  "url": "https://jobs.lever.co/leverdemo/33538a2f-d27d-4a96-8f05-fa4b0e4d940e",
  "applyUrl": "https://jobs.lever.co/leverdemo/33538a2f-d27d-4a96-8f05-fa4b0e4d940e/apply",
  "description": "Welcome to the Demo Job Listing for Lever! This is a fictional job created solely for demonstration purposes...",
  "fetchedAt": "2026-08-25T03:34:28+00:00"
}
```

```json
{
  "platform": "ashby",
  "boardId": "ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "location": "New York, NY (HQ)",
  "remote": true,
  "employmentType": "FullTime",
  "postedDate": "2026-04-07",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "description": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams...",
  "fetchedAt": "2026-08-25T03:34:28+00:00"
}
```

### Typical uses

- Build one watchlist across every ATS a target company list actually uses,
  instead of three separate scrapers.
- Feed an AI agent or job board with a clean, consistent schema regardless
  of which ATS the employer picked.
- Monitor a competitor's or portfolio company's hiring by department or
  location over time.
- Pull a company's own postings into an internal dashboard without hand
  parsing three different JSON shapes.

### Input

| Field | Meaning |
| --- | --- |
| `greenhouseTokens` | Greenhouse board tokens, e.g. `"gitlab"` from `boards.greenhouse.io/gitlab`. |
| `leverSites` | Lever site slugs, e.g. `"palantir"` from `jobs.lever.co/palantir`. `"leverdemo"` is Lever's own public demo board. |
| `ashbyJobBoardNames` | Ashby job board names, e.g. `"ramp"` from `jobs.ashbyhq.com/ramp`. |
| `maxJobsPerBoard` | Cap per board, default 200. |
| `includeDescription` | Full description text, default on. Costs no extra requests on any of the three platforms. |
| `stripContactInfo` | Regex-strip emails and phone-shaped text from descriptions, default on. Courtesy best-effort, not a guarantee. |

At least one of `greenhouseTokens`, `leverSites`, or `ashbyJobBoardNames`
must be non-empty; mixing all three in one run is the normal case.

### Fair pricing

Pay per job record returned, plus a small per-board charge on every board
that is queried successfully, once pay-per-event pricing is enabled. Boards
that fail (bad token, deleted board, network error) are never charged. No
subscription.

# Actor input Schema

## `greenhouseTokens` (type: `array`):

The token in a Greenhouse job board's URL, for example "gitlab" from boards.greenhouse.io/gitlab. Find it in the company's careers page URL or by viewing page source for "boards-api.greenhouse.io".

## `leverSites` (type: `array`):

The slug in a Lever job board's URL, for example "palantir" from jobs.lever.co/palantir. "leverdemo" is Lever's own public demo board and always has postings, useful for a first test.

## `ashbyJobBoardNames` (type: `array`):

The name in an Ashby job board's URL, for example "ramp" from jobs.ashbyhq.com/ramp.

## `maxJobsPerBoard` (type: `integer`):

Caps how many job records are returned per configured board, newest-first where the platform's ordering allows it.

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

Fetch and include the full job description text. All three platforms return it in the same list request this Actor already makes per board, so turning this off saves no requests; it only trims the output size.

## `stripContactInfo` (type: `boolean`):

Regex-strips email addresses and phone-number-shaped text found inside job description text, as a courtesy default. This is a best-effort pattern match, not a guarantee that every phone number or email is caught, and it never touches any other field.

## Actor input object example

```json
{
  "greenhouseTokens": [
    "gitlab"
  ],
  "leverSites": [
    "palantir"
  ],
  "ashbyJobBoardNames": [
    "ramp"
  ],
  "maxJobsPerBoard": 200,
  "includeDescription": true,
  "stripContactInfo": true
}
```

# Actor output Schema

## `jobs` (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 = {
    "greenhouseTokens": [
        "gitlab",
        "robinhood"
    ],
    "leverSites": [
        "leverdemo",
        "palantir"
    ],
    "ashbyJobBoardNames": [
        "ramp",
        "notion"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("inn_corp/job-feed-aggregator").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 = {
    "greenhouseTokens": [
        "gitlab",
        "robinhood",
    ],
    "leverSites": [
        "leverdemo",
        "palantir",
    ],
    "ashbyJobBoardNames": [
        "ramp",
        "notion",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("inn_corp/job-feed-aggregator").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 '{
  "greenhouseTokens": [
    "gitlab",
    "robinhood"
  ],
  "leverSites": [
    "leverdemo",
    "palantir"
  ],
  "ashbyJobBoardNames": [
    "ramp",
    "notion"
  ]
}' |
apify call inn_corp/job-feed-aggregator --silent --output-dataset

```

## MCP server setup

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

```

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/xfWbvBe70FOtQ0pPf/builds/jmeKyRwDytviqCZQt/openapi.json
