# ATS Jobs API: Greenhouse, Lever, Ashby, Workday (`constanciodev/ats-jobs-worldwide`) Actor

Monitor open jobs from 10,099 companies on Greenhouse, Lever, Ashby and Workday, with seniority, area and remote-from-Brazil filters. Job postings API with salary, country and work model, searchable by company name. Monitor mode charges only new jobs. No login, no proxy.

- **URL**: https://apify.com/constanciodev/ats-jobs-worldwide.md
- **Developed by:** [Constancio Dev](https://apify.com/constanciodev) (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.50 / 1,000 job postings

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

## ATS Jobs API: Greenhouse, Lever, Ashby & Workday job postings

**Monitor open jobs from 10,099 companies on Greenhouse, Lever, Ashby and Workday, with
seniority, area and remote-from-Brazil filters.** One Actor, one output format: each job comes
back with company, location, country, work model, salary range, seniority and posting date, and
you pay only for the jobs you receive. Schedule it in monitor mode and pay only for jobs that
are new or changed since the last run.

Built by **Constancio Dev**, a small engineering studio focused on data pipelines and B2B
automation. [constanciodev.com.br](https://constanciodev.com.br)

### What you get

- **A jobs API for 5 ATS platforms.** Greenhouse, Lever, Ashby, Workday and InHire, read through
  their official public job board endpoints. No login, no proxy, no headless browser.
- **10,099 companies searchable by name.** The built-in index (checked against each public job
  board on 22/10/2026, kept only if it had open jobs) has 3,396 companies on Greenhouse, 1,353
  on Lever, 2,225 on Ashby and 3,125 on Workday. Type `Stripe` or `Capital One` instead of
  hunting for the careers URL, or paste any careers page URL or `platform:slug`.
- **Fields you can filter on.** Country (ISO code), US and Brazilian state, remote, hybrid or
  onsite, salary range and currency, seniority, area (backend, data, ML/AI, product...), years of
  experience required and whether a remote job is open to someone living in Brazil.
- **Monitor mode.** Run it on a schedule and receive only jobs that are new or changed.
- **Safe costs.** `maxItems` and the maximum cost per run stop the Actor cleanly, keeping every
  job already delivered.

### Example output

One dataset item per job. Real item from a local test run (22/10/2026):

```json
{
  "id": "lever:ciandt:8d33fe81-e0c7-4708-9d69-2f1ee89daad3",
  "platform": "lever",
  "company": "CI&T",
  "title": "[Job - 31975] .NET Developer Senior, Brazil",
  "url": "https://jobs.lever.co/ciandt/8d33fe81-e0c7-4708-9d69-2f1ee89daad3",
  "location": "Brazil",
  "country": "BR",
  "work_model": "remote",
  "department": "Centelha",
  "posted_at": "2026-10-22T17:18:11.741000+00:00",
  "salary_min": null,
  "salary_max": null,
  "seniority": "senior",
  "area": "backend",
  "remote_scope": "remote_country_specific",
  "remote_open_to_brazil": true,
  "years_experience": null,
  "scraped_at": "2026-10-22T18:00:39.302156+00:00"
}
```

The real item also has `company_slug`, `state`, `employment_type`, `salary_currency`,
`salary_period` and, unless you turn it off, `description`. A field the posting does not support
is `null`, never guessed.

| Field | Meaning |
|---|---|
| `id` | Platform, company and job id. Stable across runs, good for deduplication. |
| `platform`, `company_slug` | Which ATS and which board the job came from |
| `country`, `state` | ISO 3166 country code; US state or Brazilian state code when the location allows it |
| `work_model` | `remote`, `hybrid` or `onsite`, as declared by the ATS or written in the location |
| `salary_*` | Range as the posting states it (or parsed from the description): currency code and period (`year`, `month`, `hour`...) |
| `seniority` | `intern`, `junior`, `mid`, `senior`, `staff_plus` (staff, principal, tech lead), `manager` (manager, head, director, VP) or `unknown` |
| `area` | `backend`, `frontend`, `fullstack`, `data`, `ml_ai`, `devops_sre`, `mobile`, `security`, `qa`, `product`, `design` or `other` |
| `remote_scope` | `remote_global`, `remote_latam_or_americas`, `remote_country_specific`, `hybrid`, `onsite` or `unknown` |
| `remote_open_to_brazil` | `true` when a person living in Brazil can take the job remotely, `false` when not, `null` when the posting does not say |
| `years_experience` | Years of experience required, when stated |

### Input examples

**1. Remote senior engineering jobs a person in Brazil can take**

```json
{
  "companies": ["greenhouse:anthropic", "ashby:openai", "lever:spotify", "Stripe", "Databricks"],
  "remoteOpenToBrazil": true,
  "seniority": ["senior", "staff_plus"],
  "area": ["backend", "fullstack", "data", "ml_ai"],
  "maxItems": 200
}
```

**2. Daily feed of new jobs for your site or newsletter (schedule it once a day)**

```json
{
  "companies": ["Stripe", "Capital One", "NVIDIA", "ashby:ramp"],
  "keywords": ["data engineer", "analytics engineer"],
  "locations": ["United States", "Remote"],
  "includeDescription": false,
  "monitorMode": true
}
```

**3. Hiring signal for sales: who is recruiting sales roles this week**

```json
{
  "allIndexedCompanies": true,
  "keywords": ["account executive", "sales development"],
  "postedWithinDays": 7,
  "includeDescription": false,
  "maxItems": 5000
}
```

The third example reads the whole index, which is a long run: raise the run timeout (see
[Limits](#limits)).

#### Input reference

| Field | What it does |
|---|---|
| `companies` | Careers page URLs, `platform:slug`, or a company name from the index. Empty collects a curated list of 154 companies (Stripe, OpenAI, NVIDIA...). |
| `allIndexedCompanies` | Add every company of the index to the list. |
| `keywords` | Keep jobs whose title contains at least one of these words (case and accents ignored). |
| `locations` | Keep jobs whose location contains one of these, or whose country matches (`London`, `Germany`, `US`). |
| `remoteOnly` | Keep only jobs the posting declares as remote. |
| `seniority`, `area` | Keep only these levels or areas (values in the table above). |
| `remoteOpenToBrazil` | Keep only remote jobs that a person living in Brazil can take. |
| `postedWithinDays` | Keep only jobs posted in the last N days. Jobs without a date are left out when set. |
| `maxJobsPerCompany` | Newest jobs kept per company after the filters (default 100, 0 = no limit). |
| `maxItems` | Stop after delivering this many jobs in the run (default 50,000, 0 = no limit). |
| `includeDescription` | Include the full description text (default: true). |
| `monitorMode` | Deliver and charge only jobs that are new or changed since the previous run. |
| `seenWindowDays` | Monitor mode: forget a job after it has not been listed for this many days (default 90). |

Limits per run: up to 200 `companies`, 100 `keywords` and 100 `locations`; `postedWithinDays` up
to 365, `maxJobsPerCompany` up to 2,000 and `maxItems` up to 1,000,000.

Accepted company formats:

| Platform | URL | `platform:slug` |
|---|---|---|
| Greenhouse | `https://boards.greenhouse.io/stripe`, `https://job-boards.greenhouse.io/stripe` | `greenhouse:stripe` |
| Lever | `https://jobs.lever.co/spotify` | `lever:spotify` |
| Ashby | `https://jobs.ashbyhq.com/openai` | `ashby:openai` |
| Workday | `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | `workday:nvidia.wd5/NVIDIAExternalCareerSite` |
| InHire | `https://olist.inhire.app/vagas` | `inhire:olist` |

### Pricing

Pay per event: **$1.50 per 1,000 jobs** delivered to the dataset, plus $0.00005 per GB of
memory when the run starts. Companies that return no jobs cost nothing, and neither do jobs
removed by your filters or, in monitor mode, jobs you already received. Set a maximum cost per
run in the run options and the Actor stops when it reaches it.

### FAQ

**Is it allowed to collect these jobs?** The Actor reads only postings that companies publish
openly through the public job board endpoints of their ATS, without login, and respects
`robots.txt`. Check the terms that apply to your own use of the data.

**How does monitor mode work?** The Actor keeps a list of the jobs it delivered in a Key-Value
Store of your account, with the date each one was last seen. A job that is still listed and has
not changed is not delivered or charged again. A job that changed its title, location, salary or
description comes back. A job not listed for `seenWindowDays` days (default 90) is forgotten, so
the list does not grow forever.

**Why is a job's `remote_open_to_brazil` null?** The posting says only "Remote" with no country,
region or time zone, so the Actor does not guess.

**Can I use it for Brazilian companies only?** The companion Actor
[Vagas Brasil](https://apify.com/constanciodev/vagas-brasil) is tuned for jobs in Brazil, with
BRL salary and Brazilian states. For one ATS only, see
[Greenhouse](https://apify.com/constanciodev/greenhouse-jobs-scraper),
[Lever](https://apify.com/constanciodev/lever-jobs-scraper),
[Ashby](https://apify.com/constanciodev/ashby-jobs-scraper) and
[Workday](https://apify.com/constanciodev/workday-jobs-scraper).

**Something is wrong or missing?** Open an issue on this Actor's page, or write to
<suporte@constanciodev.com.br>.

### Limits

- **Only public job boards.** Jobs that a company keeps off its public ATS board, or on another
  ATS, are not covered. The index is a snapshot of 22/10/2026: a company can have changed ATS
  since, and then you get a clear failure line in the log for that company.
- **Run time.** Greenhouse, Lever and Ashby return a whole board in one request. Workday needs
  one request per job, so a Workday company with the default limit of 100 jobs takes about 3
  minutes; up to 10 companies run at a time. A local run of the curated list with no filters
  (16/10/2026) took 16 minutes and delivered 12,237 jobs. The full index holds about 880,000
  open jobs, far more than the default 1 hour timeout covers: raise the timeout and keep
  `maxItems` as the cap. When the timeout is near, the Actor stops cleanly and keeps what it
  delivered.
- **Classification is rules, not AI.** `seniority`, `area`, `remote_scope` and
  `remote_open_to_brazil` come from deterministic rules over the title, location and
  description (see below). A title with no level and no years of experience is `unknown`.
- **Salary only when it is written.** It comes from the ATS pay fields or the description text;
  many postings have none.
- **Monitor memory is shared.** All monitor runs in your account use one list. Two schedules with
  different filters over the same companies share it, so a job delivered by one is not delivered
  by the other.
- **No contact fields.** The output has no recruiter name, e-mail or phone field; the description
  is the posting text exactly as the company published it.
- **Politeness.** It identifies itself, waits 1.5 s between requests to the same host and backs
  off on HTTP 429/503. Workday sites whose `robots.txt` closes the careers site are left out.

### Job classification

Every job gets four extra fields, computed inside the Actor by deterministic rules over the
title, location, work model and description. No paid API, no network call, no AI model: the
same posting always gets the same answer, and it adds about half a second per 1,000 jobs.

- **`seniority`** reads the title first (`Senior`, `Sr.`, `III`, `Staff`, `Principal`,
  `Tech Lead`, `Engineering Manager`, `Head of`, `Director`, `Intern`, `New Grad`, `II`,
  Portuguese `Pleno` and `Estagio` too). "Product Manager", "Account Manager" and "Program
  Manager" are individual roles, not people managers. With no level in the title, the years of
  experience the posting asks for decide: up to 1 year is `junior`, 2 to 4 is `mid`, 5 or more
  is `senior`. With neither, `unknown`.
- **`area`** reads the title. Sales, support, recruiting and other non engineering roles are
  `other`, and so is an engineering title that names no specialty (plain "Software Engineer").
- **`remote_scope`** combines the ATS work model, the location text and phrases in the
  description ("3 days a week in the office", "fully remote", "#LI-Hybrid"). A remote job
  limited to a country or region (US, UK, EMEA, Europe, APAC) is `remote_country_specific`; one
  open to Latin America or the Americas is `remote_latam_or_americas`; one that says worldwide,
  global or "international OK", or names no country at all, is `remote_global`.
- **`remote_open_to_brazil`** is `true` for remote jobs open worldwide (explicitly), in Latin
  America or the Americas, in Brazil, or in a time zone range that includes UTC-3 (for example
  "UTC-6 to UTC+3"). It is `false` for hybrid and onsite jobs and for remote jobs limited to
  another country. A bare "Remote" with no country stays `null`.

Measured accuracy, on 150 real jobs from this Actor's own output that were labeled by hand
without looking at the rules (another 424 labeled jobs were used to write them):

| Field | Accuracy |
|---|---|
| `seniority` | 89.3% |
| `area` | 96.7% (87.5% on the 32 tech jobs alone) |
| `remote_scope` | 95.3% |
| `remote_open_to_brazil` | 98.7% |

A TF-IDF and logistic regression model trained on the same 424 jobs scored lower on every
field (51% to 96%), so the Actor ships the rules only.

### Data sources and credits

The company index is built by `scripts/descobrir_empresas.py`, which reads candidate company
slugs from these openly licensed public lists (each pinned to a commit) and then checks every
candidate against the ATS's own public job board API. Only the validated slugs are kept; no
job text from these lists is copied.

| List | License | Commit |
|---|---|---|
| [vanshb03/Summer2027-Internships](https://github.com/vanshb03/Summer2027-Internships) | MIT, Copyright (c) 2025 Vansh Bataviya | `ec34b41` and 5 earlier commits |
| [vanshb03/New-Grad-2027](https://github.com/vanshb03/New-Grad-2027) | MIT, Copyright (c) 2025 Vansh Bataviya | `acefe9d` and 4 earlier commits |
| [dreamworkhq/Tech-Internships-2027](https://github.com/dreamworkhq/Tech-Internships-2027) | MIT, Copyright (c) 2026 Dreamwork | `f7893c0` |
| [dreamworkhq/Open-Tech-Internships-2027](https://github.com/dreamworkhq/Open-Tech-Internships-2027) | MIT, Copyright (c) 2026 Dreamwork | `dab60d4` |
| [TrevorDohm/MBA-Tech-Internships-2027](https://github.com/TrevorDohm/MBA-Tech-Internships-2027) | CC0-1.0 | `e73d9b5` |
| [ambicuity/New-Grad-Jobs](https://github.com/ambicuity/New-Grad-Jobs) | MIT, Copyright (c) 2026 Ritesh Rana | `51e543d` |
| [Feashliaa/job-board-aggregator](https://github.com/Feashliaa/job-board-aggregator) | MIT, Copyright (c) 2026 Riley Dorrington | `136fa9b` |

The MIT-licensed lists are used under the terms of the MIT License, which requires keeping
their copyright and permission notices; see each repository's `LICENSE` file.

# Changelog

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

# Actor input Schema

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

Careers page URLs or 'platform:slug', for example https://boards.greenhouse.io/stripe, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/openai, https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite, 'greenhouse:stripe' or 'workday:nvidia.wd5/NVIDIAExternalCareerSite', or a company name from the built-in index ('Stripe', 'Capital One'). Leave empty to collect the curated list of 150+ companies on Greenhouse, Lever, Ashby and Workday.

## `allIndexedCompanies` (type: `boolean`):

Add every company of the built-in index (10,099 companies validated on 22/10/2026) to the list. This is a long run: about 880,000 open jobs in total, and Workday needs one request per job, so an unfiltered run takes many hours. Jobs are delivered company by company while the run goes on. Narrow it with keywords, locations, postedWithinDays or seniority, keep maxItems as the total cap, and raise the run timeout in Run options (the default 1 hour collects only part of the index). When the timeout is near or your max charge is reached, the run stops cleanly and keeps every job already delivered.

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

Keep jobs whose title contains at least one of these words (case and accents ignored). Empty keeps every job.

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

Keep jobs whose location contains one of these, or whose country matches it: 'London', 'Germany', 'US', 'Remote'. Empty keeps every location.

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

Keep only jobs the posting declares as remote.

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

Keep only jobs classified with one of these levels (from the title, or from the years of experience the posting asks for). Empty keeps every level.

## `area` (type: `array`):

Keep only jobs classified in one of these areas, read from the title. Empty keeps every area.

## `remoteOpenToBrazil` (type: `boolean`):

Keep only remote jobs that a person living in Brazil can take: remote worldwide, remote in Latin America or the Americas, remote in Brazil, or a time zone range that includes UTC-3. Jobs with no clear evidence are left out.

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

Keep only jobs posted in the last N days. Jobs with no posting date are left out when this is set. Empty or 0 keeps every date.

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

Newest jobs kept per company after the filters; 0 means no limit. Workday needs one request per job, so the limit also keeps Workday runs short (Workday itself stops at 2,000 jobs per company).

## `maxItems` (type: `integer`):

Stop after delivering this many jobs in the run. 0 means no limit (the run then stops only at your max charge or at the run timeout).

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

Turn off for a smaller dataset with only the structured fields.

## `monitorMode` (type: `boolean`):

For scheduled runs: deliver and charge only jobs that are new or changed since the previous run in your account.

## `seenWindowDays` (type: `integer`):

Monitor mode only. A job that is still listed is never delivered or charged again unless it changes. A job that has not been listed for this many days is forgotten, so if it comes back it counts as new.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "lever:spotify",
    "ashby:openai",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "allIndexedCompanies": false,
  "keywords": [
    "engineer"
  ],
  "remoteOnly": false,
  "remoteOpenToBrazil": false,
  "maxJobsPerCompany": 100,
  "maxItems": 50000,
  "includeDescription": true,
  "monitorMode": false,
  "seenWindowDays": 90
}
```

# Actor output Schema

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

Every job posting delivered in this run, as dataset items.

# 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",
        "lever:spotify",
        "ashby:openai",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
    ],
    "keywords": [
        "engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("constanciodev/ats-jobs-worldwide").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",
        "lever:spotify",
        "ashby:openai",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    ],
    "keywords": ["engineer"],
}

# Run the Actor and wait for it to finish
run = client.actor("constanciodev/ats-jobs-worldwide").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",
    "lever:spotify",
    "ashby:openai",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "keywords": [
    "engineer"
  ]
}' |
apify call constanciodev/ats-jobs-worldwide --silent --output-dataset

```

## MCP server setup

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

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/R14q88oG6FzyhrFll/builds/OXFJgP7NskY32sGRE/openapi.json
