# ATS Job Feed — Greenhouse, Lever, Ashby, Workday & More (`contributory_garnet_hyz/ats-job-feed`) Actor

Jobs straight from six public ATS APIs (Greenhouse, Lever, Ashby, Workable, Personio, Workday) — no scraping, no login, no keys. Normalises salaries to annual USD, de-duplicates across boards, and can return only jobs you have not seen yet.

- **URL**: https://apify.com/contributory_garnet_hyz/ats-job-feed.md
- **Developed by:** [Radar Labs](https://apify.com/contributory_garnet_hyz) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 jobs

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 Job Feed — jobs straight from company career pages

Most "job scraper" actors screen-scrape a job board. This one doesn't scrape at all: it reads the **same public JSON endpoints the companies' own career pages use**, on six applicant-tracking systems, all of which are keyless and login-free.

> Numbers, limits and public sample data: <https://sjdfi0wjvosdju9.github.io/>

| ATS | Endpoint used | Typical companies |
| --- | --- | --- |
| Greenhouse | `boards-api.greenhouse.io/v1/boards/{slug}/jobs` | Stripe, GitLab, Figma, Datadog, Duolingo |
| Lever | `api.lever.co/v0/postings/{slug}` | many mid-size startups |
| Ashby | `api.ashbyhq.com/posting-api/job-board/{slug}` | OpenAI, Notion, Linear |
| Workable | `apply.workable.com/api/v1/widget/accounts/{slug}` | SMBs |
| Personio | `{slug}.jobs.personio.de/xml` | German-speaking mid-market |
| Workday | `{tenant}.wd{number}.myworkdayjobs.com/wday/cxs/...` | large enterprises (NVIDIA, Salesforce, Intel) |

You give it company names. It works out which of the six each company uses (404 on one, try the next), and returns a single flat table.

### Look at real output before you pay

No sign-up needed — this is a live 200-row sample from a default run, readable anonymously:

- **JSON**: <https://api.apify.com/v2/datasets/foLhhiBMhnWcmxZJv/items?format=json&clean=true>
- **CSV**: <https://api.apify.com/v2/datasets/foLhhiBMhnWcmxZJv/items?format=csv&clean=true>

Every row carries `company`, `ats`, `title`, `location`, `remote`, `department`, `postedAt`, and — where the company publishes one — an annual `salaryLow`/`salaryHigh` in local currency plus `salaryUsdLow`/`salaryUsdHigh`.

### Why it is built this way

I wrote up the measurements behind this Actor — 527 guessed slugs, 259 live boards, 18,825
postings, 8,071 with a pay range, and the two bugs that published salaries off by 12x and
2,080x — in a post you can read before spending anything:

**[Six APIs, zero keys: what 18,825 real job postings contain](https://dev.to/sdijjxcfs/six-apis-zero-keys-what-18825-real-job-postings-contain-4a7a)**

### What makes the output usable

- **Salary is normalised, not guessed.** Ashby and Lever publish salary as structured numbers, so they are read as numbers, never regex-guessed: currency, interval and min/max come from the API.
  Greenhouse and Workday publish pay inside the job description instead, so those are read from the text — but only from a small window around a pay cue (`salary range`, `base pay`, `compensation range`, `per hour`, …), and only when the unit word sits right next to the amount. On a full sweep of 260 companies this lifted salary coverage from **15.6% to 44% of all rows** (4,207 Greenhouse rows alone).
  Everything is converted to an **annual** range (`per hour` × 2080, `per month` × 12) and also reported in USD using a fixed exchange-rate table.
- **No naked numbers.** When a salary has to be read out of text, the number must sit next to a currency symbol or currency code. Without that rule, a job description mentioning "970 employees" becomes a $970 salary — which is how most scrapers produce garbage salary columns.
- **De-duplicated across boards.** The same role posted under two locations, or listed twice by the company, collapses into one row, keeping the most complete version.
- **Failure is reported, not hidden.** Companies that return nothing land in `failedCompanies` in the summary with the reason. One dead slug never aborts the run.

### Input

| Field | Default | Notes |
| --- | --- | --- |
| `companies` | 13 well-known slugs | slugs or full career-page URLs (Workday **needs** the URL) |
| `keywords` | none | match title, department or summary |
| `ats` | `auto` | force one ATS if you already know it |
| `remoteOnly` | `false` | location or workplace type says remote |
| `minSalaryUsd` | `0` | drops jobs **without** a published salary when set |
| `includeDescriptions` | `true` | download Greenhouse job bodies — the only source of Greenhouse salaries, and the main cost |
| `salaryOnly` | `false` | drop every job with no published pay range — see below |
| `excludeSenior` / `excludeJunior` / `excludeNonTech` | `false` | title-based role filters |
| `maxAgeDays` | `0` | 0 keeps evergreen postings |
| `maxJobs` | `200` | output cap after ranking |
| `maxPerCompany` | `20` | stops one high-paying employer from flooding the feed (0 = off) |
| `onlyNew` | `false` | skip every job this account already received in an earlier run |
| `resetSeen` | `false` | forget the history first, so the next run returns everything again |
| `workdayPayRanges` | `true` | open up to 20 Workday postings per company to recover the pay range |
| `useVerifiedPool` | `false` | scan the bundled **266 verified companies** instead of your own list |
| `maxWorkers` | `6` | companies fetched in parallel (1–10); output keeps the input order |

#### The 266 companies behind `useVerifiedPool`

We scanned **527 candidate slugs** against all six boards, kept the **259** that answered, then re-probed 267 misses with spelling variants and added **7 more** — **266** boards in total. The latest full scan read **18,670 jobs**, of which **8,040 (43%)** carried a pay range, across **165 companies that publish pay at least once**. Every number below is regenerated weekly by the Actor's own pipeline (`docs/COMPANIES.md` is generated from the same pool file), so it cannot drift from what the code actually ships.

| Board | Companies | Jobs | With salary | Share |
| --- | ---: | ---: | ---: | ---: |
| Greenhouse | 139 | 11,754 | 5,101 | 43% |
| Ashby | 100 | 5,335 | 2,735 | 51% |
| Lever | 23 | 1,810 | 164 | 9% |
| Workable | 4 | 121 | 0 | see note |

Workable's four boards are real but their public widget endpoint rate-limits by IP; at the time of this scan they answered `429` and are reported as `rate-limited: …` in `failedCompanies` instead of being counted as "no pay". The other three boards are measured, not estimated.

Top 40 by number of salary-bearing jobs:

| Company | Board | Jobs | With salary |
| --- | --- | ---: | ---: |
| `openai` | Ashby | 808 | 644 |
| `anthropic` | Greenhouse | 679 | 589 |
| `databricks` | Greenhouse | 902 | 486 |
| `elastic` | Greenhouse | 391 | 250 |
| `snowflake` | Ashby | 354 | 241 |
| `braze` | Greenhouse | 331 | 235 |
| `oscar` | Greenhouse | 245 | 232 |
| `brex` | Greenhouse | 265 | 206 |
| `affirm` | Greenhouse | 224 | 196 |
| `coinbase` | Greenhouse | 225 | 182 |
| `ramp` | Ashby | 170 | 163 |
| `airwallex` | Ashby | 510 | 160 |
| `klaviyo` | Greenhouse | 164 | 160 |
| `plaid` | Ashby | 132 | 126 |
| `robinhood` | Greenhouse | 156 | 125 |
| `mongodb` | Greenhouse | 388 | 124 |
| `scaleai` | Greenhouse | 173 | 118 |
| `instacart` | Greenhouse | 123 | 113 |
| `fivetran` | Greenhouse | 166 | 108 |
| `perplexity` | Ashby | 127 | 107 |
| `gitlab` | Greenhouse | 231 | 102 |
| `baseten` | Ashby | 105 | 100 |
| `figma` | Greenhouse | 139 | 81 |
| `headway` | Ashby | 89 | 81 |
| `cohere` | Ashby | 114 | 77 |
| `clickhouse` | Ashby | 202 | 76 |
| `adyen` | Greenhouse | 244 | 75 |
| `chime` | Greenhouse | 73 | 71 |
| `mercury` | Greenhouse | 72 | 71 |
| `asana` | Greenhouse | 105 | 70 |
| `vercel` | Greenhouse | 95 | 70 |
| `faire` | Greenhouse | 71 | 70 |
| `temporal` | Ashby | 70 | 67 |
| `lyrahealth` | Lever | 473 | 64 |
| `flexport` | Greenhouse | 193 | 64 |
| `deepgram` | Ashby | 89 | 64 |
| `ripple` | Greenhouse | 96 | 61 |
| `tailscale` | Greenhouse | 59 | 53 |
| `swordhealth` | Greenhouse | 54 | 50 |
| `vanta` | Ashby | 84 | 48 |

…and 219 more, including `zocdoc`, `lightningai`, `sofi`, `pagerduty`, `sardine`, `smartsheet`, `sentry`, `modal`, `kong`, `togetherai`, `onetrust`, `hex`, …

Nothing here is a partnership or an endorsement — every number comes from the companies' own public job feeds, measured on a single scan day. Companies add and remove boards, so a slug that works today can 404 tomorrow; the Actor reports those per company instead of hiding them.

A full-pool scan measured **18,825 rows in ~80 seconds** with `maxWorkers: 6` and only one
dead slug (`calm`, which 404s on all six boards). Because each pool entry already knows
which ATS answers, the Actor skips the probing step, so a wide scan costs one request per
company instead of up to four.

That is a lot of rows, so pair it with the filters:

```json
{
  "useVerifiedPool": true,
  "keywords": ["engineer"],
  "minSalaryUsd": 120000,
  "maxJobs": 100,
  "maxPerCompany": 5
}
```

### If you do not need job descriptions (`includeDescriptions`)

Greenhouse is the only one of the six boards that hides salary ranges inside the job body, and those bodies are also
where most of the payload sits. Set `includeDescriptions: false` to skip them.

Measured on the bundled pool at the time of that test (259 boards, before the pool was widened to 266 and before the de-duplication fix — use the ratios, not the absolute counts):

| | run time (local, 6 workers) | jobs with a salary | platform cost (same input) |
| --- | ---: | ---: | ---: |
| `includeDescriptions: true` (default) | 138 s | 6,838 / 15,530 (44%) | $0.00451 |
| `includeDescriptions: false` | 66 s | 2,834 / 15,531 (18%) | **$0.00083** |

Two honest consequences:

- Greenhouse salaries disappear (Ashby and Lever publish salaries as structured fields, so they are unaffected).
- Because ranking prefers jobs with a salary, the top of the list shifts towards Ashby. That is not a bug; it is what
  "cheap mode" costs.

`salaryOnly` and `minSalaryUsd` force this back on — filtering by pay needs the pay.

### If you only care about jobs that state a salary (`salaryOnly`)

Paid feeds are noisy: roughly **43%** of the postings in our 266-company pool publish a pay range,
so a wide scan returns mostly jobs you cannot price. Set `salaryOnly: true` and the Actor drops
everything else before ranking:

```json
{
  "useVerifiedPool": true,
  "salaryOnly": true,
  "keywords": ["engineer"],
  "maxJobs": 100
}
```

Two things make this worth knowing:

- **It makes runs cheaper, not just cleaner.** You are charged per returned job, so 100 priced jobs
  cost the same as 100 unpriced ones — the filter costs nothing and saves you reading rows you cannot use.
- **It is honest about coverage.** If a board publishes almost no pay (Workable, and Greenhouse outside
  the ~45% that write it in the description), `salaryOnly` simply returns nothing from it. `SUMMARY`
  reports scraped vs. survived, so you can see which companies are actually worth asking for.

The per-board publication rates and a cost calculator (rows in, dollars out) live here:
<https://sjdfi0wjvosdju9.github.io/salary-only.html>. It is a static page with no tracking, and the numbers
come from the same run that produces the free CSV, so you can check our arithmetic before paying us.

### Only pay for jobs you have not seen (`onlyNew`)

Put this Actor on a schedule with `onlyNew: true` and it remembers which postings it already gave you. Every later run returns **only the roles that appeared since the last run** — you are not charged again for the same 200 rows every morning.

How it behaves, measured on the live platform:

| Run | Setup | Rows returned | Jobs skipped as already seen |
| --- | --- | ---: | ---: |
| 1st | `onlyNew` + `resetSeen` | 10 | 0 |
| 2nd | `onlyNew`, nothing changed upstream | **0** | 10 |
| 3rd | `onlyNew`, nothing changed upstream | **0** | 10 |

- The fingerprint of a job is company + normalised title + URL, so punctuation and casing changes do not fake a "new" posting.
- The first run returns everything and records it. If you raise `maxJobs` later, the rows you did **not** get yet still count as new, so nothing is lost.
- History keeps the **50,000** most recent fingerprints per account (a few hundred KB in your key-value store).
- `resetSeen: true` wipes the history before running — handy after a long pause.
- If the state store cannot be opened, the run still succeeds and simply behaves as if `onlyNew` was off, rather than failing.

### Output

One dataset row per job:

`id` · `company` · `ats` · `title` · `url` · `location` · `department` · `employmentType` · `postedAt` · `remote` · `isSenior` · `isJunior` · `isNonTech` · `salaryLow` · `salaryHigh` · `salaryCurrency` · `salaryUsdLow` · `salaryUsdHigh` · `summary`

Plus a `SUMMARY` key-value record with `scraped`, `afterFilters`, `returned`, `failedCompanies`, `onlyNew`, `skippedSeen`, `seenTracked`, and `byAts` / `byCompany` breakdowns.

Free bonus: because it re-reads the company's live board on every run, a **schedule** turns this into a change feed — new roles show up as new rows. Add `onlyNew: true` and the change feed is also the cheap option: you only pay for rows that are actually new.

#### Workday needs a URL, and its list API has no salary

Workday is the ATS most large enterprises use, but a company name is not enough to
find its board: it needs a tenant, a `wd` cluster and a site name. Pass the careers
URL instead, for example:

```
https://acme.wd5.myworkdayjobs.com/en-US/AcmeCareers
```

A short built-in registry covers `nvidia`, `salesforce`, `visa` and `intel`, so those
plain names work too.

The Workday list endpoint also refuses `limit` above 20, so the Actor pages through in
20s. It publishes **no salary field** in the list response, but the pay range is almost
always written in the posting itself, so the Actor opens up to **20 postings per Workday
company** (newest first), reads the description, and recovers the range there. On NVIDIA
that fills in 10 of the 20 opened postings, e.g. `196,000 – 310,500 USD`. Turn this off
with `workdayPayRanges: false` if you only want the list and would rather not spend the
extra requests.

Two things worth knowing about the prose extraction:

- Only numbers next to a pay cue (`salary range`, `base pay`, `compensation range`,
  `hourly rate`, …) count, so a `$5,000 referral bonus` is not mistaken for a salary.
- When a posting lists **two bands** ("184,000 USD – 287,500 USD for Level 4, and
  224,000 USD – 356,500 USD for Level 5"), the **first band is reported**. The row is
  still honest — it is a range the employer published for that role — but the upper band
  is not merged in.

#### Currency conversion uses live rates, and never guesses

Postings quote pay in whatever the company uses. We keep the original numbers
(`salaryLow`, `salaryHigh`, `salaryCurrency`) and add a `salaryUsd*` value converted
with a dated rate table fetched once per run and cached for 24 hours. The rate date is
reported as `fxDate` in the SUMMARY record, so a number can always be traced to a day.

When a currency has no rate, the USD fields stay `null` instead of being estimated.
Until this change only ten currencies converted, so a posting quoted in PLN, AED, MXN
or RON came back looking like it had no salary at all - and `salaryOnly` / `minSalaryUsd`
quietly dropped it. On the bundled 266-company pool that hid about 1,170 real postings.

### Honest limits

- **Greenhouse publishes no salary *field*** — pay, when a company discloses it, lives in the description. We parse it there (about 45% of Greenhouse rows on our sweep), so a Greenhouse row is either correctly priced or empty, never a guess. Ashby and Lever are still the most consistent: nearly every row has a range.
- **We drop implausible numbers instead of shipping them.** A range that annualises below $5,000 or above $5,000,000 (or $1.5M in USD) is thrown away and the job goes out with an empty salary. Two real examples that this guard caught: a monthly figure labelled USD that annualised to $1,536,000, and an annual figure that got ×2080 because the word "hourly" appeared elsewhere in a long description.
- **Workable's public widget endpoint is often an empty shell** (it returns `{"jobs": []}` for some accounts). Those companies will show up in `failedCompanies` rather than pretending to have no openings.
- **Evergreen roles exist.** Some boards leave a posting up for years, so `postedAt` is not a freshness guarantee.
- **No salary negotiation data, no headcount, no recruiter contacts.** This is the company's own published feed, nothing more.
- Most company slugs work out of the box, but a company that uses none of these six ATSes comes back as failed. A wider sweep we ran over 527 candidate slugs hit **260 companies**; on the earlier metadata-only sweep nearly all salary-bearing rows came from **Ashby** boards, but reading Greenhouse descriptions brings its 135 boards to **4,207 salary-bearing rows**, so both are worth adding.

### Pricing

Pay per event: **`job-returned`** — one charge per job that survives your filters. Filtered-out and duplicate jobs are not charged. A `apify-actor-start` charge applies once per run.

### Compliance

All four endpoints are the public JSON APIs that the companies' own career pages call. Nothing here logs in, submits applications, or touches anything behind a login. Respect the terms of the ATS providers if you redistribute the data at scale.

### Local development

```bash
cp .env-example .env      # APIFY_TOKEN=...
python -m src             # same entrypoint the container uses
python tests/test_ats_core.py   # offline unit tests, no network
```

### The same data as a repo

The CSV behind this Actor is also published as an open dataset (CC BY 4.0) at <https://github.com/sjdfi0wjvosdju9/job-salary-open-data>.

# Actor input Schema

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

Company slugs or career-page URLs. A slug is usually just the lowercase company name (try "stripe", "gitlab", "openai"). Full careers URLs also work and are required for Workday boards, e.g. "https://acme.wd5.myworkdayjobs.com/en-US/AcmeCareers". Greenhouse, Lever, Ashby, Workable, Personio and Workday are supported and detected automatically.

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

Only keep jobs whose title, department or summary contains at least one of these words (case-insensitive). Leave empty for everything.

## `ats` (type: `string`):

Skip auto-detection and query one ATS only. Leave on Auto unless you already know the answer.

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

Keep only jobs whose location or workplace type says remote.

## `salaryOnly` (type: `boolean`):

Drop every job that has no published pay range. About 43% of the bundled pool has one, so this typically cuts the result set by more than half — use it when pay is the whole point and you would rather get 40 priced jobs than 200 unpriced ones.

## `minSalaryUsd` (type: `integer`):

Keep only jobs whose published salary reaches this annual USD amount. Jobs with no published salary are dropped when this is set — that is the point of the filter.

## `excludeSenior` (type: `boolean`):

Drop titles containing senior, staff, principal, lead, head, director, VP, manager, architect.

## `excludeJunior` (type: `boolean`):

Drop titles containing junior, intern, internship, entry level, graduate, apprentice.

## `excludeNonTech` (type: `boolean`):

Drop sales, marketing, recruiting, finance, legal, support and operations titles.

## `maxAgeDays` (type: `integer`):

Drop postings older than this. Leave 0 to keep everything — many ATS boards leave evergreen roles up for years.

## `maxJobs` (type: `integer`):

How many jobs to output after filtering and ranking (jobs with a published salary first, then higher salary, then newer).

## `maxPerCompany` (type: `integer`):

Cap how many rows a single company can take in the output, so one high-paying employer cannot flood the feed. Set 0 to disable the cap.

## `onlyNew` (type: `boolean`):

Skip every job this Actor already returned in a previous run by the same account. Meant for scheduled runs: you only pay for genuinely new postings instead of re-buying the same list every day. The first run returns everything and remembers it; history is capped at the 50,000 most recent fingerprints.

## `resetSeen` (type: `boolean`):

Clear the remembered jobs for this account before running, so the next run returns everything again. Use it after a long pause, or if you want a full list once more.

## `workdayPayRanges` (type: `boolean`):

Workday's list API has no salary field, so this opens up to 20 Workday job pages per company and reads the pay range from the description. It is the only way to get a salary for Workday companies, but it costs extra requests — turn it off if you only want the job list.

## `useVerifiedPool` (type: `boolean`):

Ignore the companies list above and scan the bundled pool instead: 260 companies that were all verified live (18,813 jobs, 2,944 with a published salary). Each entry already knows which ATS answers, so there is no probing. A wide scan like this returns thousands of rows — pair it with keywords, minSalaryUsd and maxJobs to keep the output and the bill small.

## `maxWorkers` (type: `integer`):

How many companies to fetch in parallel. Higher is faster on big lists; lower is gentler on the ATS endpoints. Results keep the input order either way.

## `includeDescriptions` (type: `boolean`):

Greenhouse boards only ship salary ranges inside the job body. Downloading bodies costs bandwidth: a full-pool run measured 138s with bodies vs 66s without, and salary coverage 44% vs 18%. Ashby and Lever publish salaries as structured fields, so they are unaffected. Forced on when salaryOnly or minSalaryUsd is set.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "gitlab",
    "openai",
    "figma"
  ],
  "keywords": [
    "engineer"
  ],
  "ats": "auto",
  "remoteOnly": false,
  "salaryOnly": false,
  "minSalaryUsd": 0,
  "excludeSenior": false,
  "excludeJunior": false,
  "excludeNonTech": false,
  "maxAgeDays": 0,
  "maxJobs": 200,
  "maxPerCompany": 20,
  "onlyNew": false,
  "resetSeen": false,
  "workdayPayRanges": true,
  "useVerifiedPool": false,
  "maxWorkers": 6,
  "includeDescriptions": true
}
```

# Actor output Schema

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

One row per job posting, with company, title, location, department, remote flag, annualised salary in original currency and USD.

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

How many jobs were scraped, how many survived the filters, which companies were reachable, and the per-ATS breakdown.

# 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": [
        "stripe",
        "gitlab",
        "openai",
        "figma"
    ],
    "keywords": [
        "engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("contributory_garnet_hyz/ats-job-feed").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": [
        "stripe",
        "gitlab",
        "openai",
        "figma",
    ],
    "keywords": ["engineer"],
}

# Run the Actor and wait for it to finish
run = client.actor("contributory_garnet_hyz/ats-job-feed").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": [
    "stripe",
    "gitlab",
    "openai",
    "figma"
  ],
  "keywords": [
    "engineer"
  ]
}' |
apify call contributory_garnet_hyz/ats-job-feed --silent --output-dataset

```

## MCP server setup

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

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/zqBGjpzG8z3UZxN51/builds/QfV43pRbJd7enc1e5/openapi.json
