# ATS Jobs Hiring Signals: Greenhouse, Lever & Ashby API (`optimistprime/ats-jobs-hiring-signals`) Actor

ATS jobs from Greenhouse, Lever and Ashby public APIs: salaries, remote flag, hiring signals and new-job alerts as clean JSON. No login.

- **URL**: https://apify.com/optimistprime/ats-jobs-hiring-signals.md
- **Developed by:** [Derek VanLaningham](https://apify.com/optimistprime) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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 Jobs Hiring Signals: Greenhouse, Lever & Ashby API

**Get every open job from any company on Greenhouse, Lever or Ashby: clean JSON, salaries, hiring signals, no login.**

Turn a list of companies into every open job they're hiring for, in one run. This Actor reads the **official public job-board APIs** of Greenhouse, Lever and Ashby, so it's fast, stable and needs no login or proxies. Paste board URLs or just slugs (`stripe`, `ramp`) and get clean, typed JSON: title, department, locations, remote/hybrid, salary range (normalized to annual), posting date and apply link. Add a per-company **hiring-signal summary** (open roles, new in the last 7/30 days, top departments) or turn on **monitor mode** to get only new and removed jobs on a schedule.

### Ready-to-run examples

Each example below is a **published task**: open its page and click **Try** to copy it into your account and run it, or paste the JSON into the Input tab (JSON view). Times, rows and costs are from real runs (FREE-tier prices; hiring summaries Oct 3 on 1.0.10, Figma and remote data jobs Oct 5 on 1.0.14); results change as the source data changes.

#### Figma Engineering Jobs with Salary Ranges (Greenhouse API)

[Open this example](https://apify.com/optimistprime/ats-jobs-hiring-signals/examples/figma-engineering-jobs-with-salaries): 3 s, 11 rows, $0.0185. All 11 rows had a salary range.

```json
{"companies":["figma"],"ats":["greenhouse"],"keywords":["engineer"],"descriptionFormat":"none","maxTotalJobs":11}
```

Output excerpt (one row, selected fields):

```json
{
 "title": "Data Platform Engineer",
 "companyName": "Figma",
 "department": "Engineering",
 "locations": [
  "San Francisco, CA • New York, NY • United States",
  "Canada",
  "US"
 ],
 "salary": {
  "min": 153000,
  "max": 376000,
  "currency": "USD",
  "interval": "year",
  "minAnnual": 153000,
  "maxAnnual": 376000,
  "summary": "Annual Base Salary Range:",
  "source": "greenhouse"
 },
 "postedAt": "2026-09-21T16:13:46.000Z",
 "applyUrl": "https://boards.greenhouse.io/figma/jobs/6201407004?gh_jid=6201407004"
}
```

#### Hiring Signals: Stripe (Greenhouse), Palantir (Lever), Ramp

[Open this example](https://apify.com/optimistprime/ats-jobs-hiring-signals/examples/greenhouse-lever-ashby-hiring-summaries): 3 s, 3 rows, $0.017.

```json
{"companies":["https://boards.greenhouse.io/stripe","https://jobs.lever.co/palantir","https://jobs.ashbyhq.com/ramp"],"outputMode":"summary_only"}
```

Output excerpt (one row, selected fields):

```json
{
 "recordType": "company_summary",
 "companyName": "Ramp",
 "ats": "ashby",
 "openJobs": 157,
 "newJobsLast30Days": 54,
 "remoteShare": 0.09,
 "salaryDisclosedShare": 0.96,
 "medianSalaryMinAnnualUSD": 168000,
 "topDepartments": [
  {
   "name": "Sales",
   "count": 62
  },
  {
   "name": "Engineering",
   "count": 38
  },
  {
   "name": "People & Talent",
   "count": 13
  },
  {
   "name": "Design",
   "count": 7
  },
  {
   "name": "Product",
   "count": 7
  }
 ]
}
```

#### Remote Data Jobs at Stripe, Ramp, Figma, Notion & Databricks

[Open this example](https://apify.com/optimistprime/ats-jobs-hiring-signals/examples/remote-data-jobs-at-tech-companies): 5 s, 11 rows, $0.0185. Uses `"remoteMatch": "eligible"`: jobs where remote is at least one listed option (`remoteEligible`). The default `strict` match returned 2 rows.

```json
{"companies":["stripe","ramp","figma","notion","databricks","plaid"],"keywords":["data"],"remoteOnly":true,"remoteMatch":"eligible","descriptionFormat":"none","maxTotalJobs":11}
```

Output excerpt (one row, selected fields):

```json
{
 "title": "Specialist Solutions Architect, Data",
 "companyName": "Stripe",
 "ats": "greenhouse",
 "primaryLocation": "SF, NY, Remote",
 "workplaceType": null,
 "remoteEligible": true,
 "postedAt": "2026-08-27T00:33:39.000Z",
 "applyUrl": "https://stripe.com/jobs/search?gh_jid=8144262"
}
```

### What you get

One dataset row per job with: `title`, `companyName`, `department`, `team`, `employmentType`, `workplaceType` (remote / hybrid / onsite), `locations`, `country`, `salary` (min, max, currency, interval, **annualized** min/max), `postedAt`, `updatedAt`, `jobUrl`, `applyUrl`, `description` (text / markdown / html / none) and stable IDs (`jobKey`).

```json
{
  "recordType": "job",
  "jobKey": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "ats": "ashby",
  "boardSlug": "ramp",
  "companyName": "Ramp",
  "companyNameSource": "slug",
  "atsJobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "full_time",
  "workplaceType": "hybrid",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)"],
  "primaryLocation": "New York, NY (HQ)",
  "country": "USA",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year", "minAnnual": 211400, "maxAnnual": 290600, "summary": "$211.4K – $290.6K • Offers Equity", "source": "ashby" },
  "postedAt": "2026-04-07T17:12:35.753Z",
  "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",
  "description": "About Ramp ...",
  "descriptionTruncated": false,
  "language": null,
  "sourceApiUrl": "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true",
  "scrapedAt": "2026-10-03T15:30:00.000Z",
  "isNew": null
}
```

### Why this Actor

- **Official public APIs**: the same JSON endpoints companies use to embed their job boards. Stable, ToS-friendly, no bot-fighting, no login, no proxy needed.
- **Three ATSs in one call**: Greenhouse, Lever (incl. EU) and Ashby, normalized into one schema.
- **Auto-detects the slug**: pass `stripe` and it checks all three ATSs in parallel (keeps every board that exists).
- **Salaries normalized**: min/max, currency, interval, plus `minAnnual`/`maxAnnual` (never currency-converted).
- **Hiring signals**: one summary row per company (open jobs, new in 7/30 days, remote share, top departments/locations, salary disclosure, median USD salary).
- **Monitor mode**: only new jobs since the last run, plus `job_removed` rows for jobs that disappeared.
- **Cheap per-job pricing**, and **MCP-ready** for AI agents (stable camelCase keys, every key always present, `null` when unknown).

### Use cases

- **Sales / GTM**: hiring = buying intent. "Which of my 500 target accounts are hiring data engineers this month?"
- **Recruiting / sourcing**: track openings at target companies.
- **Job boards and aggregators**: bulk, normalized feeds.
- **Market and comp research**: salary ranges, remote share, department mix.
- **AI agents**: "Is X hiring for Y?" in one tool call.

### How to use the Greenhouse, Lever and Ashby jobs API

1. **Add companies.** Paste job-board URLs (`https://boards.greenhouse.io/stripe`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/ramp`) or just slugs (`stripe`, `ramp`) into **Companies**. Slugs are checked on Greenhouse, Lever and Ashby automatically (limit this with **ATS**).
2. **Filter (optional).** Use **Keywords** (e.g. `engineer`, `data`), **Departments**, **Location contains**, **Employment types**, **Remote only** and **Posted within days**.
3. **Pick the output.** `jobs` returns one row per job, with salary ranges converted to annual figures where the company publishes pay. `summary_only` returns one hiring-signal row per company (open jobs, new jobs in the last 7 and 30 days, remote share, top departments and locations, salary disclosure). `jobs_and_summary` returns both.
4. **Run it.** One request per job board, so most runs finish in seconds. Example: 1,000 SpaceX jobs in 5.4 s.
5. **Monitor (optional).** Turn on **Only new jobs** with a **Monitor ID** and schedule the Actor daily to get only jobs you haven't seen.
6. **Use it from AI agents.** The Actor works as a tool through Apify's MCP server (`mcp.apify.com`) for Claude, Cursor and other MCP clients.

You pay per result: $1.50 per 1,000 jobs on the Free plan (less on paid plans), plus $0.002 per run start. Data comes only from the companies' public job-board APIs. No login or proxy is needed.

**Example inputs**

**Single company**

```json
{ "companies": ["https://boards.greenhouse.io/stripe"] }
```

**Bulk list, small payload**

```json
{ "companies": ["stripe", "ramp", "palantir", "figma", "notion"], "descriptionFormat": "none", "outputMode": "jobs_and_summary" }
```

**Filtered remote engineering**

```json
{ "companies": ["ramp", "figma"], "keywords": ["engineer"], "excludeKeywords": ["intern"], "remoteOnly": true, "postedWithinDays": 30 }
```

**Monitor mode (schedule daily)**

```json
{ "companies": ["stripe", "ramp", "palantir"], "onlyNewJobs": true, "monitorId": "my-accounts", "descriptionFormat": "none" }
```

Create a [schedule](https://docs.apify.com/platform/schedules) for this input. The **first** run outputs every job as new (that's expected); later runs output only new jobs and `job_removed` rows.

### Input reference

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | string\[] (required) | – | ATS board URLs, ATS API URLs or bare slugs. Max 1000. |
| `ats` | string\[] | all three | Which ATSs to try for bare slugs (`greenhouse`, `lever`, `ashby`). URLs always use their own ATS. For a bare slug, Lever is tried on `api.lever.co` first and then on the EU host `api.eu.lever.co`. The EU fallback is **only tried for bare slugs**. A `jobs.lever.co/...` URL is looked up only on the global host, so paste the `jobs.eu.lever.co/...` URL for an EU board. |
| `outputMode` | `jobs` | `jobs_and_summary` | `summary_only` | `jobs` | Job rows, job rows + one summary per company, or summaries only. Summary stats cover the **whole board**, except `matchingJobs`, which counts only the jobs that pass your filters. |
| `keywords` | string\[] | `[]` | Keep jobs whose title contains any of these (case-insensitive). With `keywordScope: "title_and_description"`, the description is searched too. |
| `keywordScope` | `title` | `title_and_description` | `title` | Where `keywords` match. `title_and_description` searches the full job description as well, even with `descriptionFormat: "none"` (no extra cost). `excludeKeywords` always match the title only. |
| `excludeKeywords` | string\[] | `[]` | Drop jobs whose title contains any of these. |
| `locationContains` | string\[] | `[]` | Keep jobs where any location contains one of these. |
| `departments` | string\[] | `[]` | Keep jobs whose `department` or `team` contains any of these (case-insensitive), e.g. `Engineering`. Jobs with neither field are dropped when set. |
| `employmentTypes` | string\[] | `[]` | Keep only these normalized `employmentType` values: `full_time`, `part_time`, `contract`, `intern`, `temporary`, `other`. Jobs with an unknown type are kept too (see next row). Listing `unknown` always keeps them. |
| `includeUnknownEmploymentType` | boolean | `true` | Only used with `employmentTypes`. `true` also keeps jobs whose ATS publishes no employment type (`employmentType: null`, e.g. most Greenhouse boards). `false` drops them, unless `unknown` is listed. |
| `remoteOnly` | boolean | `false` | Keep only remote jobs (see `remoteMatch`). |
| `remoteMatch` | `strict` | `eligible` | `strict` | With `remoteOnly`: `strict` keeps `workplaceType == "remote"`; `eligible` keeps `remoteEligible == true`. |
| `postedWithinDays` | integer | `0` | Only jobs first published within N days (0 = no limit; undated jobs are kept only when 0). |
| `descriptionFormat` | `none` | `text` | `markdown` | `html` | `text` | Description format. `none` is smallest. |
| `maxDescriptionChars` | integer | `0` | Truncate descriptions (sets `descriptionTruncated`). |
| `maxJobsPerCompany` | integer | `0` | Cap job rows per company after filters. In monitor mode, new jobs cut off by this cap stay "new" and are output by the next run. |
| `maxTotalJobs` | integer | `0` | Global cap on job rows (cost control). In monitor mode, new jobs cut off by this cap stay "new" and are output by the next run. |
| `onlyNewJobs` | boolean | `false` | Monitor mode: output only jobs not seen in earlier runs with the same `monitorId`, plus free `job_removed` rows. With `outputMode: "summary_only"`, the seen-jobs list is **not** updated. |
| `monitorId` | string | `default` | Monitor state name (letters, digits, `-`). |
| `proxyConfiguration` | proxy | off | Not needed for these official APIs. |

### Output reference

Every row has `recordType`. Filter on it first. All keys of a record type are always present.

**`job`**

| Field | Type | Notes |
|---|---|---|
| jobKey | string | `{ats}:{boardSlug}:{atsJobId}`, globally unique |
| ats | `greenhouse`|`lever`|`ashby` | |
| boardSlug | string | lower-case |
| companyName / companyNameSource | string|null / `ats`|`slug` | Greenhouse provides the name; Lever/Ashby use the title-cased slug |
| atsJobId, title | string | |
| department, team | string|null | |
| employmentType | `full_time`|`part_time`|`contract`|`intern`|`temporary`|`other`|null | Greenhouse: always null (not standardized) |
| workplaceType | `remote`|`hybrid`|`onsite`|null | Lever/Ashby: from the board's workplace field. Greenhouse has no such field: `remote` only when every location in the name says remote (`US-Remote`, `Remote US`); office lists that also mention remote (`SF, NYC, Remote`) and all other jobs are null |
| remoteEligible | boolean | `true` when the workplace field says remote **or any** listed location mentions remote |
| locations / primaryLocation | string\[] / string|null | |
| country | string|null | Lever ISO-2, Ashby as given, Greenhouse null |
| salary | object|null | `{min, max, currency, interval, minAnnual, maxAnnual, summary, source}` |
| postedAt / updatedAt | ISO 8601 UTC|null | |
| jobUrl / applyUrl | string / string|null | |
| description / descriptionTruncated | string|null / boolean | per `descriptionFormat` |
| language | string|null | Greenhouse only |
| sourceApiUrl, scrapedAt | string | |
| isNew | boolean|null | `true` in monitor mode, else null |

**`company_summary`**: `ats, boardSlug, companyName, boardUrl, openJobs` (all jobs on the board), `matchingJobs` (after your filters), `newJobsLast7Days, newJobsLast30Days, remoteShare` (0–1; Lever/Ashby: over jobs with a known workplace type, null if none; Greenhouse: clearly-remote jobs ÷ all jobs, since Greenhouse has no workplace field), `topDepartments, topLocations` (top 5 `{name, count}`), `salaryDisclosedShare` (0–1), `medianSalaryMinAnnualUSD`, `scrapedAt`. Stats other than `matchingJobs` are computed over all open jobs on the board.

**`error`** (free): `input, ats, boardSlug, errorCode` (`BOARD_NOT_FOUND`, `NOT_DETECTED`, `INVALID_INPUT`, `UPSTREAM_ERROR`, `RATE_LIMITED`), `message, scrapedAt`. One row per company entry that returned nothing, so you (or your agent) can see why.

**`job_removed`** (free, monitor mode): `jobKey, ats, boardSlug, title, jobUrl, lastSeenAt, scrapedAt`.

**Remote: strict vs eligible.** `workplaceType: "remote"` is strict (the source says the job is remote, and on Greenhouse every listed location says remote); `remoteEligible: true` is loose (remote is at least one option, e.g. `SF, NYC, Seattle, Remote`). Stripe, Oct 3: 35 strict vs 114 eligible of 715.

**Salary notes**: Ashby uses the first `Salary` component of `compensation.compensationTiers`. Lever uses `salaryRange`. Greenhouse uses the first of `pay_input_ranges[]` (`min_cents`/`max_cents` ÷ 100, `currency_type`, `title` as summary). Greenhouse has no interval field. `interval` is `hour` when the range title (or, failing that, the blurb) mentions an hour ("hour", "hourly", "per hour", "/hr"). It is `year` when it mentions a year ("year", "yearly", "annual", "per annum"). Otherwise it is null, and then `minAnnual`/`maxAnnual` are null too. Annualization: year ×1, month ×12, week ×52, day ×260, hour ×2080. Example from Figma: title "Annual Base Salary Range:" with `min_cents` 16500000 gives `{min: 165000, max: 190000, currency: "USD", interval: "year", minAnnual: 165000, maxAnnual: 190000}`. Greenhouse only returns pay ranges when the request includes `pay_transparency=true`. The Actor always sends it on the same single request (no extra calls). Currencies are never converted.

### Pricing

Pay per event: you only pay for what you get. No platform usage charges.

| Event | Price (Free tier) | When |
|---|---|---|
| `job` | $0.0015 | per job row |
| `company-summary` | $0.005 | per company summary row |
| `company-checked` | $0.0005 | per board checked, **monitor mode only** |
| `apify-actor-start` | $0.002 | once per run (charged by Apify) |

Higher Apify plans get lower per-job prices (down to $0.0010 per job). `error` and `job_removed` rows are free.

Worked examples: 1,000 jobs ≈ **$1.50** on the Free tier. Checking 100 companies daily in monitor mode ≈ **$0.05/day** + $0.0015 per new job. "Is X hiring?" for 50 companies with `summary_only` ≈ $0.25.

Set **Max cost per run** to cap spending. The Actor stops cleanly when it's reached ("Stopped: your max-cost-per-run limit was reached after N jobs") and you keep everything delivered so far.

### Use as an MCP tool / with AI agents

Add it to any MCP client (Claude, Cursor, …) via the Apify MCP server:

```
https://mcp.apify.com/?tools=OptimistPrime/ats-jobs-hiring-signals
```

Example tool call:

```json
{ "companies": ["ramp"], "descriptionFormat": "none" }
```

Tips for agents:

- Use `descriptionFormat: "none"` (or `maxDescriptionChars: 1000`) for large lists to keep payloads small.
- Use `outputMode: "summary_only"` for "is company X hiring?" questions (one row per company).
- Use `maxTotalJobs` to bound cost and response size.
- Check `recordType: "error"` rows to learn why a company returned nothing (e.g. not on these ATSs).

### Integrations

**API (JavaScript)**

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('OptimistPrime/ats-jobs-hiring-signals').call({ companies: ['stripe', 'ramp'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**API (Python)**

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("OptimistPrime/ats-jobs-hiring-signals").call(run_input={"companies": ["stripe", "ramp"]})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

- **Schedules + webhooks**: run monitor mode daily and POST new jobs to your CRM or Slack.
- **Make, Zapier, n8n**: use the Apify app/node, "Run Actor" + "Get dataset items".
- **Google Sheets**: export the `overview` view as CSV, or use the Google Sheets integration.

### Limits and FAQ

- **How is this different from other Greenhouse, Lever or Ashby scrapers on Apify?** Other Greenhouse/Lever/Ashby Actors on the Store cost between $0.0015 and $0.004 per job and are reliable. Choose this one if you want all three ATSs in one run, normalized salaries, per-company hiring-signal summaries, or new-jobs-only monitoring.
- **Which companies are covered?** Only companies whose job board is hosted on Greenhouse, Lever or Ashby. Companies on Workday, iCIMS, SmartRecruiters, Workable etc. are not covered in v1.
- **How do I find a company's slug?** Open their careers page and click a job. If the URL is `boards.greenhouse.io/<slug>`, `job-boards.greenhouse.io/<slug>`, `jobs.lever.co/<slug>` or `jobs.ashbyhq.com/<slug>`, paste that URL. Company career-site URLs (e.g. `stripe.com/jobs`) return a `NOT_DETECTED` row in v1.
- **Do summary stats respect my filters?** Only `matchingJobs` does. It counts the jobs that pass `keywords` (with `keywordScope`), `excludeKeywords`, `locationContains`, `departments`, `employmentTypes`, `remoteOnly` and `postedWithinDays`. `openJobs`, `newJobsLast7Days`/`newJobsLast30Days`, `remoteShare`, `topDepartments`, `topLocations`, `salaryDisclosedShare` and `medianSalaryMinAnnualUSD` describe the whole board.
- **Am I charged for jobs my filters drop?** No. The `job` event is charged only for job rows that reach your dataset after **all** filters and caps. Filtered-out jobs are free.
- **`employmentTypes` and Greenhouse:** Lever and Ashby publish an employment type for each job. Greenhouse has no standard field; it is filled only when a board publishes it as custom metadata (e.g. "Time Type: Full Time"), otherwise `employmentType` is `null`. By default these unknown-type jobs are **kept** when you filter by `employmentTypes`, so Greenhouse boards don't silently disappear. Set `includeUnknownEmploymentType: false` for a strict filter.
- **Does `excludeKeywords` search descriptions?** No, it always matches the title only, also with `keywordScope: "title_and_description"`.
- **`departments` matching:** a case-insensitive substring of `department` or `team`, so `Engineering` also matches "Platform Engineering". Greenhouse boards sometimes prefix departments with cost-centre numbers (e.g. "7112 Data Science"); substring matching handles that. Greenhouse only returns departments and offices together with job content, so the Actor always requests it (still one request per board, no extra charge). With `descriptionFormat: "none"` the description HTML is dropped right after download and never output.
- **Keywords in descriptions:** with `keywordScope: "title_and_description"`, a job matches if any keyword is in the title or the full description text. Matching uses the full text even when the output description is `none`, HTML or truncated. On Greenhouse this fetches the job content in the same single request, so runs move a little more data but cost the same.
- **Monitor mode + `maxJobsPerCompany`/`maxTotalJobs`:** only jobs that were actually delivered (and charged) are marked as seen. New jobs that a cap cut off stay "new", so the next run outputs them. Nothing is skipped, and nothing is charged twice.
- **Monitor mode + `summary_only`:** a summaries-only run delivers no job rows, so it doesn't touch the seen-jobs list. It marks nothing as seen and emits no `job_removed` rows. To advance monitoring, run a full jobs pass (`outputMode` `jobs` or `jobs_and_summary`) with the same `monitorId`.
- **EU Lever boards:** the EU host (`api.eu.lever.co`) is only tried as a fallback for **bare slugs**. For a Lever URL, the URL's own host is used. Paste `https://jobs.eu.lever.co/<slug>` for EU boards.
- **First monitor run outputs everything.** That's how monitoring starts; later runs only output changes. Jobs are only marked removed for boards fetched successfully in that run.
- **Data freshness**: live; each run reads the boards at run time.
- **Rate limits**: requests are throttled to 2/s per API host with retries; you shouldn't need a proxy.

### Legal / ethical note

This Actor reads public job postings from official job-board APIs that companies publish for embedding. It collects no personal data and uses no login. You are responsible for how you use the data downstream. Not affiliated with, or endorsed by, Greenhouse, Lever or Ashby.

### Support / changelog

Open an issue in the **Issues** tab; we aim to reply within 24 hours. See [CHANGELOG.md](./CHANGELOG.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/optimistprime/ats-jobs-hiring-signals/changelog.md

# Actor input Schema

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

Companies to fetch jobs for. Each entry can be an ATS board URL (e.g. https://boards.greenhouse.io/stripe, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/ramp), an ATS API URL, or a bare board slug (e.g. 'stripe'). Bare slugs are auto-detected across Greenhouse, Lever and Ashby. Max 1000 entries.

## `ats` (type: `array`):

Which ATS platforms to try when an entry is a bare slug. URLs always use the ATS they point to.

## `outputMode` (type: `string`):

'jobs' = one row per job. 'jobs_and_summary' = job rows plus one hiring-signal summary row per company. 'summary_only' = only one summary row per company (cheapest way to check who is hiring).

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

Keep only jobs whose title contains at least one of these (case-insensitive). Empty = all jobs.

## `keywordScope` (type: `string`):

Where 'keywords' are matched: 'title' (default) or 'title_and_description' (title or full job description). Works with any descriptionFormat, including 'none'; no extra cost. excludeKeywords always match the title only.

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

Drop jobs whose title contains any of these (case-insensitive).

## `locationContains` (type: `array`):

Keep only jobs where any location string contains one of these (case-insensitive), e.g. 'Remote', 'New York', 'Germany'.

## `departments` (type: `array`):

Keep only jobs whose department or team contains at least one of these (case-insensitive), e.g. 'Engineering', 'Sales'. Jobs with no department/team are dropped when set. Empty = all jobs.

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

Keep only these normalized employment types: full_time, part_time, contract, intern, temporary, other. Jobs whose ATS publishes no employment type (common on Greenhouse) are kept too unless you turn off 'Include unknown employment type'. Listing 'unknown' always keeps them. Empty = all jobs.

## `includeUnknownEmploymentType` (type: `boolean`):

Only used when employmentTypes is set. true (default) also keeps jobs whose ATS publishes no employment type (employmentType null, e.g. most Greenhouse boards). false drops them, unless 'unknown' is listed in employmentTypes.

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

Keep only jobs whose workplaceType is 'remote'. Greenhouse has no workplace field, so there a job counts as remote only when every location in its location name says remote (e.g. 'US-Remote').

## `remoteMatch` (type: `string`):

Only used when Remote only is on. 'strict' (default, unchanged behaviour) keeps jobs whose workplaceType is 'remote'. 'eligible' keeps jobs whose remoteEligible flag is true: the workplace field says remote OR any listed location mentions remote (e.g. Greenhouse 'SF, NYC, Seattle, Remote').

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

Keep only jobs first published within the last N days. 0 = no limit. Jobs without a publish date are kept only when this is 0.

## `descriptionFormat` (type: `string`):

'none' (smallest, best for AI agents/lead lists), 'text', 'markdown', or 'html'.

## `maxDescriptionChars` (type: `integer`):

Truncate the description to this many characters (0 = no truncation). Truncated rows get descriptionTruncated=true.

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

Cap job rows per company (after filters). 0 = no cap.

## `maxTotalJobs` (type: `integer`):

Global cap on job rows for the whole run. 0 = no cap. Use this to control cost. Prefilled 11 so a first try costs under $0.02; set 0 for every job.

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

Output (and charge for) only jobs not seen in previous runs with the same monitorId. Also emits 'job_removed' rows for jobs that disappeared since the last run.

## `monitorId` (type: `string`):

Name for the monitor state (letters, digits, '-'). Runs with the same monitorId share 'seen jobs' history. Default: 'default'.

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

Not needed for these official APIs. Leave off unless you hit rate limits.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "ramp"
  ],
  "ats": [
    "greenhouse",
    "lever",
    "ashby"
  ],
  "outputMode": "jobs",
  "keywords": [],
  "keywordScope": "title",
  "excludeKeywords": [],
  "locationContains": [],
  "departments": [],
  "employmentTypes": [],
  "includeUnknownEmploymentType": true,
  "remoteOnly": false,
  "remoteMatch": "strict",
  "postedWithinDays": 0,
  "descriptionFormat": "text",
  "maxDescriptionChars": 0,
  "maxJobsPerCompany": 0,
  "maxTotalJobs": 11,
  "onlyNewJobs": false,
  "monitorId": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `summaries` (type: `string`):

No description

## `allRows` (type: `string`):

No description

## `runSummary` (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/stripe",
        "https://jobs.lever.co/palantir",
        "ramp"
    ],
    "maxTotalJobs": 11
};

// Run the Actor and wait for it to finish
const run = await client.actor("optimistprime/ats-jobs-hiring-signals").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/stripe",
        "https://jobs.lever.co/palantir",
        "ramp",
    ],
    "maxTotalJobs": 11,
}

# Run the Actor and wait for it to finish
run = client.actor("optimistprime/ats-jobs-hiring-signals").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/stripe",
    "https://jobs.lever.co/palantir",
    "ramp"
  ],
  "maxTotalJobs": 11
}' |
apify call optimistprime/ats-jobs-hiring-signals --silent --output-dataset

```

## MCP server setup

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

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/xwadmejw1bDRKSoqj/builds/VDRdIvoTMYtoDvMuf/openapi.json
