# Greenhouse, Lever & Ashby Jobs Finder by Company Domain (`technoirglobal/ats-jobs-finder`) Actor

Turn company domains into open jobs from public Greenhouse, Lever, Ashby, Workable and Recruitee boards. Normalized fields, salary where the ATS publishes it, remote flag, and an 'only new since last run' monitoring mode. Pay only for matched companies and saved jobs.

- **URL**: https://apify.com/technoirglobal/ats-jobs-finder.md
- **Developed by:** [Tech Noir Global](https://apify.com/technoirglobal) (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 results

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?

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

**Turn a list of company domains into their open jobs from Greenhouse, Lever, Ashby, Workable and Recruitee.** Run it weekly to get only the new roles at the companies you watch: hiring signals for sales, job alerts for recruiters, or one clean jobs table for analysis.

| | |
|---|---|
| **You give it** | Company domains (`stripe.com`), job-board URLs, or `ats:slug` (`greenhouse:datadog`) |
| **You get** | One row per open job (title, department, location, remote flag, posted date, apply link; salary and description where the board provides them), plus a status row per company: matched, unverified, not found or error, and why |
| **You pay** | $0.005 per matched company + $0.0012 per job delivered ($1.20 per 1,000). No start fee. Companies with no board cost $0 |

**Quick start**

1. Paste your company domains into **Companies**.
2. Optional: add **Title or department keywords** such as `sales` or `account executive`.
3. Click **Start**, then open the **Output** tab. To monitor, turn on **Only new jobs since last run** and add a weekly **Schedule**.

### Weekly hiring-signal monitor

Watch target accounts for new sales roles every Monday:

```json
{
  "companies": ["stripe.com", "datadoghq.com", "notion.so", "ramp.com", "gitlab.com"],
  "titleKeywords": ["sales", "account executive"],
  "onlyNewJobs": true,
  "stateKey": "target-accounts-sales"
}
```

1. Save this input as an Apify **Task** and add a **Schedule** (for example `0 8 * * 1`, Mondays 08:00 in the schedule's time zone).
2. The first run returns every open job whose **title or department** contains a keyword. Later runs return only jobs not delivered before. Use one `stateKey` per watchlist.
3. The **Company summary** shows, per company: status, board, open roles, jobs delivered, new since last run.

### What you can use it for

- **Hiring signals for sales and GTM:** know when target accounts open roles like "Head of Security".
- **Job alerts:** new roles at the companies you care about.
- **Recruiting and market maps:** where competitors hire and which teams grow.
- **Salary benchmarking:** collect published pay ranges from Ashby, Lever and Recruitee boards.

### How much does it cost to scrape Greenhouse, Lever and Ashby jobs?

| Event | Price | Charged when |
|---|---|---|
| Company matched | $0.005 | Once per company whose board was accepted and read |
| Job delivered | $0.0012 | Once per job saved to the dataset |

**Per run: $0.005 × matched companies + $0.0012 × jobs delivered.** No start fee, no charge for summary records.

- Unmatched, unverified and errored companies cost nothing, and so do jobs your filters drop.
- Monitoring reruns still cost $0.005 per matched company, even with no new jobs: 50 matched companies with nothing new cost $0.25.
- Cap any run with Apify's **Max cost per run** setting.

**Real example (20 well-known companies, no filters, Oct 18, 2026):** 16 matched × $0.005 = $0.08, plus 3,652 jobs × $0.0012 = $4.38, for **about $4.46 total**. The other 4 (Google, Apple, Shopify, Revolut) don't hire through a supported system. Lookalike boards exist under their names, so they came back unverified, and cost $0.

### Input

`companies` takes domains, board URLs (`https://jobs.ashbyhq.com/ramp`) or `ats:slug` (`lever:palantir`). Known boards skip detection.

| Field | What it does |
|---|---|
| `titleKeywords` | Keep jobs whose title **or department** contains any keyword |
| `excludeKeywords` | Drop jobs whose title contains any of these |
| `locations`, `remoteOnly` | Location text match, or remote jobs only |
| `postedWithinDays` | Drop older jobs (where the ATS gives a date) |
| `onlyNewJobs` + `stateKey` | Monitoring mode |
| `maxJobsPerCompany` | Jobs per company (0 = no limit) |

### Output example

One dataset item per job (real item, description shortened):

```json
{
  "company": "ramp", "ats": "ashby", "companySlug": "ramp",
  "title": "Security Engineer, Cloud", "department": "Engineering", "team": "Backend",
  "location": "New York, NY (HQ)", "allLocations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "isRemote": true, "employmentType": "FullTime",
  "salary": {"min": 211400, "max": 290600, "currency": "USD", "interval": "1 YEAR"},
  "postedAt": "2026-04-07T17:12:35+00:00",
  "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...",
  "input": "ramp.com", "detectionMethod": "slug-guess-verified", "isNewSinceLastRun": null
}
```

**Company summary** (record `COMPANY_SUMMARY`, one row per input company, not billed; real row from a test run):

```json
{"domain": "ramp.com", "status": "matched", "boards": ["ashby/ramp"], "ats": "ashby", "slug": "ramp",
 "openRoles": 167, "jobsDelivered": 71, "newSinceLastRun": null, "reason": ""}
```

`status` is `matched`, `unverified`, `not_found`, `error`, or `skipped` (cost limit reached). Unverified rows list `candidateBoards`; pass one as `ats:slug` if it's right. The `SUMMARY` record lists every board checked.

### How it works

For each domain, the Actor runs the same deterministic check:

1. **Links on the company's site.** It reads the homepage, `/careers`, `/jobs`, the `careers.` and `jobs.` subdomains, and careers links one hop deeper. A linked board is used unless its own details name a different company (for example, a parent linking a subsidiary's board).
2. **Board names derived from the domain.** `datadoghq.com` gives `datadoghq` and `datadog`, checked on all 5 systems. A derived board is accepted **only if its own details point to the company's domain**; a matching name isn't enough, because parents, subsidiaries and country entities share names. Empty derived boards are never accepted: empty accounts can name any website (in testing, an empty Workable account named apple.com).
3. **Honest result.** If nothing is accepted, the company is **unverified** (a board exists but couldn't be confirmed; it's named as a candidate) or **not found** (with every board checked). If a check failed, for example an ATS server error, it's reported as **error**, not "not found".

Jobs are read through each system's public job-board endpoint and normalized into one schema. Workable jobs listed once per location are merged into one row.

### Accuracy: how it was measured

**Blind test, Oct 18, 2026, 100 companies.** One AI agent picked the companies; a second labeled each company's correct board from its own careers page, and the first cross-checked 28. Neither was the agent that built this Actor. Answers were frozen before any of the tested tools ran; a script scored them.

- **40 companies** used one of our 5 systems: **32 matched (80%), 0 wrong boards.** (One claimed board was a second name for the same board, serving identical job IDs; it was counted as correct after review, for us and one other tool alike.) All 8 misses were reported as "unverified", none silently. In 4 of them the right board was found but rejected because it lists no website. We trade that coverage for not returning the wrong company's jobs.
- **60 companies** had no supported board: **0 false matches** on our five systems (the other two tools also had 0 there).
- On the 40, two other Apify Actors matched 29 and 23, with 0 and 1 wrong boards on our five systems. Across all 100, the first also returned 3 wrong-company results on systems outside our five, which it covers and we don't. Both were capped at 1 job per company, so this compares board finding, not job-list completeness.

Limits: AI-labeled, not human; 40 positives is a small sample (roughly ±13 points at 95% confidence, an estimate); one local run of build 1.2.3's source. Later builds changed only how unmatched companies are reported, not which boards are accepted.

### Limits

- Only Greenhouse, Lever, Ashby, Workable and Recruitee. Companies on Workday, iCIMS, SuccessFactors, SmartRecruiters or their own career sites come back as "not found" or "unverified" and cost $0.
- This Actor reads structured salary only from Ashby, Lever and Recruitee, when the company publishes it.
- Workable's public endpoint has no job descriptions.

### Data sources and compliance

Job data comes only from public, no-login job-board endpoints: the [Greenhouse Job Board API](https://developers.greenhouse.io/job-board.html), [Lever Postings API](https://github.com/lever/postings-api), [Ashby Public Job Posting API](https://developers.ashbyhq.com/docs/public-job-posting-api), [Recruitee Careers Site API](https://docs.recruitee.com/reference/intro-to-careers-site-api) and Workable's public jobs widget endpoint. Company websites are read only to find board links. At most 1 request per second per host. It honors robots.txt (an unreachable robots.txt counts as "do not fetch") and doesn't export recruiter contact fields.

### FAQ

#### Is there a Greenhouse, Lever or Ashby jobs API for many companies at once?

Each system has a public job-board API per company, but you need each company's board name. This Actor finds the board from the domain and returns all companies in one schema. Like any Apify Actor, you can run it through the Apify API, on a schedule, with integrations, or from AI agents via the Apify MCP server.

#### A company I know uses Greenhouse came back "not found". Why?

Its board name may differ from its domain, and its site may not link the board where we look. Add it as `greenhouse:<board-name>`. The `SUMMARY` shows which names were tried.

#### If I add keywords later, do old jobs come back?

Yes. Jobs your filters dropped were never delivered, so they aren't remembered: widening `titleKeywords` delivers older matching roles as "new". `postedAt` shows when each was actually posted.

# Actor input Schema

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

One entry per line, each exactly one of: a company domain (stripe.com), a job-board URL (https://jobs.lever.co/acme), or an explicit board 'ats:slug' (greenhouse:datadog). Use the explicit form when you already know the board; it skips detection.

## `titleKeywords` (type: `array`):

Keep only jobs whose title or department contains any of these (case-insensitive), e.g. "sales", "account executive". You pay only for jobs that pass. Filtered-out jobs aren't saved or remembered by monitoring. Empty = all jobs.

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

Drop jobs whose title contains any of these.

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

Keep jobs whose location contains any of these, e.g. 'London', 'Germany'. Use 'remote' to also keep remote jobs.

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

Keep only jobs flagged remote by the ATS or with 'remote' in the location.

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

Drop jobs posted earlier than this many days ago (where the ATS gives a date).

## `onlyNewJobs` (type: `boolean`):

Monitoring mode: remembers job IDs in a named key-value store and outputs only jobs not seen in the previous run with the same state key. Ideal for scheduled runs.

## `stateKey` (type: `string`):

Name of the watchlist for 'only new jobs' mode. Use different keys for different watchlists.

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

Plain-text description (not available from Workable's public API).

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

0 = no limit.

## `allowSlugGuess` (type: `boolean`):

For domain entries, also derive board names from the domain (datadoghq.com -> datadoghq, datadog) and check them on all 5 ATSs. A derived board is accepted only if its jobs mention the company domain (detectionMethod='slug-guess-verified'). Boards linked from the company's own website are always checked.

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

How many companies are processed in parallel. Requests to each job-board host stay at 1 per second regardless.

## Actor input object example

```json
{
  "companies": [
    "airbnb.com",
    "https://jobs.ashbyhq.com/ramp",
    "lever:palantir"
  ],
  "titleKeywords": [],
  "excludeKeywords": [],
  "locations": [],
  "remoteOnly": false,
  "onlyNewJobs": false,
  "stateKey": "default",
  "includeDescription": true,
  "maxJobsPerCompany": 0,
  "allowSlugGuess": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One item per open job posting.

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

Detailed per-company log: matched boards, unverified candidates, not found (with every board tried), errors, plus run-level status counts.

## `companies` (type: `string`):

One row per input company: domain, status (matched, unverified, not_found, error), ATS/slug, open roles, jobs delivered, new since last run. Not billed.

# 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": [
        "airbnb.com",
        "https://jobs.ashbyhq.com/ramp",
        "lever:palantir"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("technoirglobal/ats-jobs-finder").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": [
        "airbnb.com",
        "https://jobs.ashbyhq.com/ramp",
        "lever:palantir",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("technoirglobal/ats-jobs-finder").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": [
    "airbnb.com",
    "https://jobs.ashbyhq.com/ramp",
    "lever:palantir"
  ]
}' |
apify call technoirglobal/ats-jobs-finder --silent --output-dataset

```

## MCP server setup

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

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/J48MUmnD27hYbNX9A/builds/2yY1EjArl3wUInaww/openapi.json
