# Job Search API - Greenhouse, Lever, Ashby & Workday Jobs Index (`tidytools/ats-job-search`) Actor

Search 1.4M+ live jobs from 54,000+ company career boards (Greenhouse, Lever, Ashby, Workday, Workable, Personio and 10 more ATSs) in one query, refreshed daily. Posted vs first-seen dates, reposts, duplicates, hiring companies. $1.50/1,000 jobs.

- **URL**: https://apify.com/tidytools/ats-job-search.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 85.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

### What does Job Search API do?

It searches **one index of live job postings from company career boards** — Greenhouse, Lever, Ashby, Workday, Workable, Recruitee, Personio, Teamtailor, BambooHR and 7 more applicant tracking systems — and returns the matching jobs in seconds. No company list needed: ask for "data engineer, remote, posted in the last 7 days" and get every matching job across all indexed employers.

**Input:** job title keywords plus any filters (location, country, remote, company, ATS, posted date, experience level, minimum salary). **Output:** one row per matching job (title, company, location, salary where published, dates, job and apply URL), or one row per hiring company. **Price:** $1.50 per 1,000 jobs ($0.0015 each, $1.35 on Silver, $1.20 on Gold), $3.00 per 1,000 hiring companies, plus a $0.0005 start fee per run; a search with no match, or a mistyped input, costs only the start fee.

Our crawler reads every indexed board **once a day through each ATS's official public job-board endpoint** (the feeds these vendors publish so companies can show jobs on their own sites). The Actor itself only queries the index, so runs are fast and cheap.

- 🔎 **Search 1,400,000+ live jobs from 54,000+ career boards** of 16 applicant tracking systems in one query
- 🗓️ **Refreshed daily**, with **two separate clocks**: `sourcePostedAt` (the date the employer's ATS gives) and `firstSeenAt` (when our crawl first saw the job), plus `lastSeenAt` and `removedAt` (when the job was taken down) — good for new-job alerts and hiring-trend research
- ♻️ **Reposts and duplicates flagged**: a job re-posted under a new URL, a job that came back, a posting date moved forward (`repostKind`), and the same opening listed under several URLs (`duplicateKind`; the extra copies are left out by default, see `skipDuplicates`)
- 🏢 **Hiring companies output**: the same filters, one row per company (deduplicated by company website) with the website, matching jobs, sample titles and locations, optionally with public contacts
- 🎯 **Filters**: title keywords (any of several phrases, `word*` for word starts), excluded words, companies, location text, ISO countries, remote only, ATS, employment type, posted within N days, new in the index within N days, **experience level** (internship, entry, mid, senior, lead, executive) and **minimum salary** (filtered-out jobs are free)
- 🧹 **One clean format**: title, company, location, country, remote, workplace type, employment type, experience level (`seniority`), department, salary range (when the ATS publishes one, also as a yearly amount), posting date, job URL and apply URL
- 🔗 **Links, not copies**: each row links to the original posting; full descriptions stay on the employer's site (a short snippet is included where the ATS provides plain text)
- 💵 **$1.50 per 1,000 jobs** returned, plus a $0.0005 start fee per run (default 256 MB memory = one start event)

Need live jobs of specific companies (17 ATSs, auto-detected from a website)? Use our [Career Page Jobs Scraper](https://apify.com/tidytools/ats-career-site-jobs) instead: it reads the boards at the moment of the run.

### How to use it

1. Type the job titles you want into **Job title keywords** (`data engineer`, `machine learning`), or leave it empty and filter by company, location or country only.
2. Narrow it down: **Remote jobs only**, **Countries (ISO codes)**, **Posted within (days)**, **Job boards (ATS)**, **Employment types**, **Experience level**, **Minimum salary**.
3. Choose **Output**: one row per job (default) or one row per hiring company.
4. Set **Max results** to cap the cost, click **Start**, then export the table as CSV, Excel or JSON, or call the Actor from the API.

The form is prefilled with two title keywords and 100 results (about $0.15, done in seconds).

### Input

| Field | What it does |
|---|---|
| `keywords` | Jobs whose **title** contains any of these phrases (all words, in order). `engineer*` matches Engineer and Engineering. Only letters and digits count: `C#` and `C++` are searched as the word "c", so write `C# developer` (the run adds a `keywordNotes` warning to SUMMARY for a one-letter keyword) |
| `excludeKeywords` | Skip titles containing any of these phrases |
| `companies` | Company name contains any of these phrases |
| `locations` | Location text contains any of these phrases (city, state, country, "Remote"). It is plain text matching: "Berlin" also finds Berlin, NJ and Berlin, VT; add `countries` (DE) to stay in one country |
| `countries` | Two-letter ISO codes (US, GB, DE), read from the location |
| `remoteOnly` | Only remote jobs |
| `postedWithinDays` | Posted in the last N days (jobs without a posting date are left out) |
| `newWithinDays` | First seen by our daily crawl in the last N days (for alerts: run daily with 1) |
| `companyDomains` | Only companies with these websites (`stripe.com`) |
| `ats` | greenhouse, lever, ashby, workday, workable, recruitee, personio, breezy, teamtailor, bamboohr, pinpoint, jazzhr, homerun, rippling, gem, freshteam |
| `employmentTypes` | full-time, part-time, contract, temporary, internship, apprenticeship, freelance, volunteer |
| `seniority` | Experience levels to keep: internship, entry, mid, senior, lead, executive (see "Experience level and salary") |
| `includeUnknownSeniority` | With `seniority`: also keep jobs whose title names no level |
| `minSalary`, `minSalaryCurrency`, `minSalaryPeriod` | Keep jobs whose stated salary reaches this amount (default currency USD, period year) |
| `salaryMatch` | `overlap` (default: top of the range reaches the minimum) or `guaranteed` (bottom of the range does) |
| `convertSalaryCurrency` | Compare salaries in other currencies with approximate fixed rates (default on) |
| `keepJobsWithoutSalary` | With `minSalary`: keep jobs that state no salary (default on) |
| `includeRemoved` | Also return jobs that were taken down |
| `sort` | `posted` (newest posting first) or `firstSeen` |
| `skipDuplicates` | Leave out extra copies of one opening posted under another URL (default on; set false to keep them, flagged `isDuplicate`). A posting URL is never returned twice in one run |
| `repostsOnly` | Only jobs we saw reposted |
| `output` | `jobs` (default) or `companies` (one row per hiring company) |
| `minJobsPerCompany`, `companySort`, `onlyWithWebsite` | Hiring companies: at least N matching jobs, order, only companies with a known website |
| `enrichCompanies` | Hiring companies: `contacts` (our Website Contact Extractor) or `company` (Company Website Enrichment), run once per company website |
| `maxResults` | Stop after this many jobs or companies (default 100, up to 100,000) |

```json
{
    "keywords": ["data engineer", "analytics engineer"],
    "remoteOnly": true,
    "postedWithinDays": 7,
    "maxResults": 500
}
```

### Output

One row per job (real example from the index, 2 October 2026):

```json
{
    "title": "Data Engineer, Data Quality & Provenance",
    "company": "Wayve",
    "companyDomain": "wayve.ai",
    "companyWebsite": "https://wayve.ai/",
    "location": "Leonberg, Germany",
    "country": "DE",
    "remote": false,
    "workplaceType": null,
    "employmentType": "full-time",
    "seniority": null,
    "senioritySource": null,
    "department": "Simulation, Evaluation, Validation / Simulation, Evaluation, Validation",
    "salaryMin": null,
    "salaryMax": null,
    "salaryCurrency": null,
    "salaryPeriod": null,
    "salaryAnnualMin": null,
    "salaryAnnualMax": null,
    "url": "https://jobs.ashbyhq.com/wayve/102a31c7-44a5-48bc-8c6e-1f4434e83974",
    "applyUrl": "https://jobs.ashbyhq.com/wayve/102a31c7-44a5-48bc-8c6e-1f4434e83974/application",
    "sourcePostedAt": "2026-10-02T15:25:57.680Z",
    "sourcePostedAtApproximate": false,
    "originalPostedAt": "2026-10-02T15:25:57.680Z",
    "firstSeenAt": "2026-10-02T16:21:23.777Z",
    "firstSeenIsBaseline": false,
    "indexLagHours": 0.9,
    "lastSeenAt": "2026-10-02T16:21:23.777Z",
    "removedAt": null,
    "postedAt": "2026-10-02T15:25:57.680Z",
    "postedAtApproximate": false,
    "updatedAt": null,
    "repostOf": null,
    "repostKind": null,
    "repostCount": 0,
    "reopenedCount": 0,
    "duplicateGroup": null,
    "duplicateKind": null,
    "isDuplicate": false,
    "companyDomainSource": "ats_profile",
    "companyDomainConfidence": "high",
    "snippet": "Before the detail, here's the challenge you'd help us solve. We build the embodied intelligence that moves real vehicles safely, and the ecosystem a billion machines will run on in the future. Very few people in AI can say this. Every role here, whatever the team, plugs into…",
    "ats": "ashby",
    "board": "ashby:wayve",
    "jobKey": "ashby:wayve:102a31c7-44a5-48bc-8c6e-1f4434e83974"
}
```

The run's key-value store has a `SUMMARY` record: `status` (SUCCESS, NO_RESULTS, LIMIT_REACHED, FAILED), `jobs` (or `companies`) returned, `matching` (total matches, capped at 100,000: then `matchingIsCapped` is true and messages say "100,000+"), the query sent and the amount charged.

### Hiring companies output

Set `output` to `companies` to turn any job search into a **list of companies that are hiring for it** — for recruiters, staffing agencies and B2B sales teams. Each company appears once, however many matching jobs it has (companies are grouped by their website; copies of one opening are counted once):

```json
{
    "company": "Maibornwolff Gmbh",
    "companyDomain": "maibornwolff.de",
    "companyWebsite": "https://www.maibornwolff.de/",
    "companyDomainSource": "board_link",
    "companyDomainConfidence": "high",
    "matchingJobs": 5,
    "matchingRemoteJobs": 0,
    "totalLiveJobs": 52,
    "sampleTitles": [
        "Senior Data Engineer (m/w/d)"
    ],
    "topLocations": [
        "Munich",
        "Frankfurt",
        "Berlin",
        "Hamburg",
        "Leipzig"
    ],
    "countries": [],
    "topDepartments": [
        "Data Engineering & BI / Data Science"
    ],
    "newestPostedAt": "2025-12-17T09:24:30.000Z",
    "oldestPostedAt": "2025-12-17T08:55:15.000Z",
    "newestFirstSeenAt": "2026-10-02T18:04:17.496Z",
    "careersUrl": "https://maibornwolff.jobs.personio.de/",
    "boards": [
        {
            "ats": "personio",
            "board": "personio:maibornwolff",
            "careersUrl": "https://maibornwolff.jobs.personio.de/",
            "liveJobs": 52
        }
    ],
    "enrichment": "contacts",
    "enrichmentStatus": "ok",
    "organizationName": "MaibornWolff GmbH",
    "primaryEmail": "info@maibornwolff.de",
    "emails": [
        "info@maibornwolff.de"
    ],
    "primaryPhone": "+4989544253000",
    "socialProfiles": {
        "linkedin": "https://www.linkedin.com/company/maibornwolff",
        "instagram": "https://www.instagram.com/maibornwolff",
        "youtube": "https://www.youtube.com/c/MaibornWolffGmbH",
        "facebook": "https://www.facebook.com/MaibornWolffGmbH"
    },
    "address": "Theresienhöhe 13, 80339 München, DE",
    "companyKey": "maibornwolff.de"
}
```

- **Company website**: found from the ATS profile, the careers page (redirects and links), job URLs on the company's own site, or a homepage that links back to the careers board. `companyDomainConfidence` is `high` or `medium`; companies we could not map have `companyDomain: null` (use `onlyWithWebsite` to skip them).
- **Contacts** (`enrichCompanies`): `contacts` runs our [Website Contact Extractor](https://apify.com/tidytools/website-contact-extractor) once for all company websites and adds `primaryEmail`, `emails`, `primaryPhone`, `socialProfiles`, `address`, `contactFormUrl`; `company` runs [Company Website Enrichment](https://apify.com/tidytools/company-website-enrichment) (description, industry, business model, contacts, tech stack). That Actor runs in your Apify account and charges its own price per website; `enrichmentStatus` says `ok`, `failed`, `not_returned` or `no_website` for each company. Only public company-level contact details are collected, not personal profiles.

### Experience level and salary

- **`seniority`** is one of `internship`, `entry`, `mid`, `senior`, `lead`, `executive`, or empty. It is derived, not guessed: an internship or apprenticeship employment type gives `internship`; otherwise level words in the title decide: Intern/Werkstudent → `internship`; Junior/Jr/Graduate/New Grad/Trainee or "Engineer I" → `entry`; Mid-level/Intermediate or "Engineer II" → `mid`; Senior/Sr or "Engineer III" → `senior`; Lead/Staff Engineer/Principal/Supervisor/Engineering Manager → `lead`; Director/Head of/VP/Chief/President → `executive`. Functional "manager" titles (Product, Project, Account, Marketing Manager…) do not count as people managers. `senioritySource` says `employment_type` or `title`. Many titles ("Software Engineer") name no level: the **Experience level** filter drops them unless **Also keep jobs without a known experience level** is on.
- **Minimum salary** compares only jobs whose ATS publishes a salary range (or wrote one in the posting). Periods are compared as yearly amounts (hour × 2080, day × 260, week × 52, month × 12), shown per job as `salaryAnnualMin`/`salaryAnnualMax` in the job's own currency. Other currencies are converted with fixed approximate rates (about 45 currencies, rounded autumn-2026 rates, can be some percent off) for the comparison only. Jobs without a comparable salary are kept by default.
- Both filters run on the results of the other filters (the index has no level or salary column to search), so a run with them reads whole pages and checks at most 200,000 jobs; `SUMMARY` shows `scannedJobs`, `jobsFilteredOut` and `matchingBeforeLevelSalaryFilters`. Dropped jobs are not charged. They work with the jobs output, not with the hiring-companies output.

### Dates: posted vs. first seen

- `sourcePostedAt` is the date the employer's ATS publishes (Workday gives the day only: `sourcePostedAtApproximate: true`). `originalPostedAt` keeps the first date when an employer later moves it forward.
- `firstSeenAt` is when our daily crawl first saw the job. For jobs that were already online when we started reading their board, `firstSeenIsBaseline` is `true` and `firstSeenAt` only marks the start of our observation.
- `indexLagHours` is `firstSeenAt − sourcePostedAt` for jobs that appeared after we started watching the board: how long a new job took to reach the index.

### Reposts and duplicates

- `repostKind`: `new_url_same_requisition` / `new_url_after_removal` (a job taken down and posted again under a new URL, `repostOf` = the earlier job), `reopened` (the same job came back), `date_refreshed` (the posting date moved forward; `repostCount` counts how often).
- `duplicateKind`: `same_requisition` (two URLs of one ATS requisition), `same_text` (same title, location and text), `cross_board` (the same job on two career boards of one company), `same_title_location` (same title in the same place — often several real openings, so never hidden). With `skipDuplicates` (on by default) the extra copies (`isDuplicate: true`) of the first three kinds are left out, and one posting URL listed on two boards is returned once (`SUMMARY.sameUrlDropped` counts them).

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/ats-job-search) to Claude, Cursor or any MCP client, then ask e.g. "Find remote machine learning engineer jobs posted this week at companies using Greenhouse or Ashby."

```json
{ "keywords": ["machine learning engineer"], "remoteOnly": true, "postedWithinDays": 7, "ats": ["greenhouse", "ashby"], "maxResults": 50 }
```

A run with no matching jobs costs only the start fee; if the index is temporarily unreachable the run fails without charging any jobs. Input mistakes (an unknown country code or ATS, a non-numeric "posted within" or "Max results" value, a list field given as a number, more than 50 values in one list, a yes/no field that is not true or false, a minimum salary with the hiring-companies output) never fail the run: it ends normally with one free `invalid_input` row per problem (`charged: false`) and `SUMMARY.status` `NO_RESULTS`.

### Pricing

Pay per event:

- **Job returned**: $0.0015 ($1.50 per 1,000), $1.35 per 1,000 on Silver, $1.20 per 1,000 on Gold and above
- **Run start**: $0.0005 per run
- **Hiring company** (`output` = `companies`): $0.003 per company row returned, $2.70 per 1,000 on Silver, $2.40 per 1,000 on Gold and above. Runs with no matching companies pay only the start fee. With `enrichCompanies`, the enrichment Actor charges its own price per website in your account (Website Contact Extractor: $0.002 per website where contact details were found).

Set **Max results** (and Apify's maximum charge per run) to cap the cost: the Actor stops exactly at your limit.

### Coverage and freshness

- Boards: 54,000+ working career boards (42,000+ with open jobs) of Greenhouse, Lever (US and EU), Ashby, Workday, Workable, Recruitee, Personio, Breezy HR, Teamtailor, BambooHR, Pinpoint, JazzHR, Homerun, Rippling, Gem and Freshteam, discovered from public web crawls (Common Crawl, 2024–2026); the list grows over time.
- In a 100-job sample (2 October 2026), 100 jobs were still listed on the employer's board and 100 job URLs opened.
- Each board is read once a day. A job that disappears from its board gets `removedAt` within about 24 hours (48 hours when the whole board goes offline).
- Workday shows at most 2,000 jobs per career site; for the largest Workday employers the index holds those 2,000.
- Workday posting dates are day-precise ("Posted 3 days ago"), marked `postedAtApproximate: true`.
- `country` is read from the location text; jobs with unclear locations have `country: null`.

### Limitations

- **Only employers that use one of the 16 supported ATSs** are in the index. Jobs posted only on LinkedIn, Indeed or a custom careers page are not.
- **Up to a day old.** Boards are read once a day; for live jobs of specific companies at the moment of the run, use the [Career Page Jobs Scraper](https://apify.com/tidytools/ats-career-site-jobs).
- **Keywords match the job title only**, not the description.
- **No full descriptions**: links and short snippets only.
- **Salary** is filled only when the employer's ATS publishes it; **country** is guessed from the location text and can be empty.

### FAQ

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**Is this data scraped from LinkedIn or job sites?** No. It comes only from the employers' own applicant tracking systems through their public job-board endpoints, with links back to the original postings.

**Can I get full job descriptions?** The index keeps links and short snippets only. For full descriptions of specific companies, use the [Career Page Jobs Scraper](https://apify.com/tidytools/ats-career-site-jobs) with descriptions on.

**How do I get only junior or internship jobs?** Set **Experience level** to `internship` and `entry`. The level comes from the employment type or the words in the title (no AI); jobs whose title names no level are left out unless you tick **Also keep jobs without a known experience level**. See "Experience level and salary".

**Can I set a minimum salary?** Yes: **Minimum salary** with its currency and period (e.g. 100000 USD per year, or 50 EUR per hour). Jobs that state no salary are kept by default (most employers publish pay only where the law requires it); turn **Keep jobs that state no salary** off to drop them. Jobs removed by these filters are not charged.

**Do I get the same job twice?** No: copies of one opening are left out by default (`skipDuplicates`), and one posting URL is returned once per run. Several real openings with the same title in one city are kept.

**What happens if I mistype an input?** The run ends normally with a free `invalid_input` row that says what to fix; nothing is charged except the start fee. This includes a search the index itself refuses. A search with no match is not a mistake: it ends normally with no rows and costs only the start fee. An empty search (no keywords or filters) is allowed and returns the newest jobs of the whole index, so keep Max results small.

**Something missing or wrong?** Open an issue on the Actor's Issues tab: we read every one.

# Actor input Schema

## `output` (type: `string`):

Jobs: one row per matching job. Hiring companies: the same filters, one row per company that is hiring for them (deduplicated by company website), with its website, matching job count, sample titles and locations; optionally with public contacts.

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

Jobs whose title contains ANY of these phrases (all words of one phrase, in order). A trailing \* matches word starts: "engineer\*" finds Engineer and Engineering. Leave empty for all jobs.

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

Skip jobs whose title contains any of these phrases, e.g. "senior", "intern".

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

Only jobs from companies whose name contains any of these phrases, e.g. "Stripe", "Datadog".

## `companyDomains` (type: `array`):

Only jobs from companies with these website domains, e.g. "stripe.com". Uses our company website mapping (see README).

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

Only jobs whose location text contains any of these phrases: a city, state or country as written by the company ("Berlin", "New York", "United Kingdom", "Remote").

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

Only jobs in these countries, as two-letter ISO codes (US, GB, DE). The country is read from the location text, so jobs with unclear locations are left out by this filter; use Locations for a looser match.

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

Only jobs marked remote by the ATS or with "Remote" in their location.

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

Only jobs posted in the last N days (by the date the ATS gives; Workday dates are day-precise). Jobs without a posting date are left out when this is set.

## `newWithinDays` (type: `integer`):

Only jobs our daily crawl first saw in the last N days. Good for alerts: run daily with 1.

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

Only jobs from these applicant tracking systems. Empty = all.

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

Only jobs of these types. Many boards do not state a type: those jobs are left out when this is set.

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

Keep jobs at these experience levels. The level (output field seniority) comes from an internship or apprenticeship employment type, else from words in the job title (Senior, Jr., Intern, Engineering Manager, Staff Engineer, VP, Engineer II...). A title without such words ("Software Engineer", "Product Manager") has no level and is dropped unless you tick the next option. Empty = all levels. Dropped jobs are not charged.

## `includeUnknownSeniority` (type: `boolean`):

With an experience level selected: also keep jobs whose level could not be derived (seniority null). Many titles name no level, so this keeps more jobs that may still fit.

## `minSalary` (type: `integer`):

Keep jobs whose stated salary reaches this amount (in the currency and period below), e.g. 100000 per year. Only jobs that state a salary are compared (the ATS's pay fields, or a range written in the description); see "Keep jobs that state no salary" for the others. Empty or 0 = no salary filter. Dropped jobs are not charged.

## `minSalaryCurrency` (type: `string`):

Three-letter currency code of the minimum salary, e.g. USD, EUR, GBP, CAD, INR. Salaries in other currencies are converted with built-in approximate exchange rates (see below).

## `minSalaryPeriod` (type: `string`):

The period of the minimum salary. Salaries given for another period are compared as yearly amounts: hour x 2080, day x 260, week x 52, month x 12.

## `salaryMatch` (type: `string`):

Values: overlap = the top of the job's salary range reaches your minimum (a 90,000-120,000 job passes a 100,000 minimum); guaranteed = the bottom of the range does (that job fails).

## `convertSalaryCurrency` (type: `boolean`):

On: a salary in another currency is converted with fixed approximate exchange rates (about 45 currencies, rounded rates of autumn 2026; can be some percent off) before comparing. The job's own salary fields are never converted. Off: only jobs in the minimum salary's currency are compared; others count as without a salary.

## `keepJobsWithoutSalary` (type: `boolean`):

With a minimum salary: keep jobs whose salary is not stated (or has no period, or cannot be compared) instead of dropping them. Most job boards publish salaries only where the law requires it (US states, EU pay transparency), so dropping them removes most jobs.

## `includeRemoved` (type: `boolean`):

Also return jobs that were taken down (removedAt is set). Useful for hiring-trend research.

## `sort` (type: `string`):

Order of the results.

## `skipDuplicates` (type: `boolean`):

On by default: leaves out extra copies of the same opening posted under another URL (same ATS requisition, same text, or the same job on two boards of one company). Several openings with the same title in one city are kept.

## `repostsOnly` (type: `boolean`):

Only jobs we saw reposted: a new URL for a job that was taken down, a job that came back, or a posting date that was moved forward.

## `maxResults` (type: `integer`):

Stop after this many jobs (or companies with the Hiring companies output). You pay per result returned.

## `minJobsPerCompany` (type: `integer`):

Only companies with at least this many matching jobs.

## `companySort` (type: `string`):

Order of the company rows.

## `onlyWithWebsite` (type: `boolean`):

Skip companies whose website we could not map.

## `enrichCompanies` (type: `string`):

Reads each company website once with our Website Contact Extractor (emails, phones, social profiles, contact form) or Company Website Enrichment (description, industry, contacts, tech stack). That Actor runs in your account and charges its own price per website.

## `enrichMaxPagesPerSite` (type: `integer`):

Pages read per company website when adding contacts.

## Actor input object example

```json
{
  "output": "jobs",
  "keywords": [
    "data engineer",
    "machine learning"
  ],
  "remoteOnly": false,
  "includeUnknownSeniority": false,
  "minSalaryCurrency": "USD",
  "minSalaryPeriod": "year",
  "salaryMatch": "overlap",
  "convertSalaryCurrency": true,
  "keepJobsWithoutSalary": true,
  "includeRemoved": false,
  "sort": "posted",
  "skipDuplicates": true,
  "repostsOnly": false,
  "maxResults": 100,
  "minJobsPerCompany": 1,
  "companySort": "jobs",
  "onlyWithWebsite": false,
  "enrichCompanies": "none",
  "enrichMaxPagesPerSite": 5
}
```

# Actor output Schema

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

No description

## `full` (type: `string`):

No description

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

No description

## `details` (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 = {
    "keywords": [
        "data engineer",
        "machine learning"
    ],
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/ats-job-search").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 = {
    "keywords": [
        "data engineer",
        "machine learning",
    ],
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("tidytools/ats-job-search").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 '{
  "keywords": [
    "data engineer",
    "machine learning"
  ],
  "maxResults": 100
}' |
apify call tidytools/ats-job-search --silent --output-dataset

```

## MCP server setup

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

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/AdBiuPJ3lCiwM0a8M/builds/9uHqtD40UIolUOJtY/openapi.json
