# Company Jobs API: Greenhouse, Lever, Ashby Openings (`stas_k/company-jobs-api`) Actor

Half of your company list can vanish without a word: no error, no row, and the summary still says every company was checked. This actor reports the ones a cut or the spending cap kept from being read, per company, with the reason. Greenhouse, Lever, Ashby and three more ATS, pay per job found.

- **URL**: https://apify.com/stas_k/company-jobs-api.md
- **Developed by:** [Stas K](https://apify.com/stas_k) (community)
- **Categories:** Jobs, Lead generation, Business
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 job founds

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

## Company Jobs API: every open role behind any domain

Paste company domains. Get every open job, in one schema, with salary when the company publishes it.

**No proxies. No cookies. No login.** The boards this actor reads are the companies' own applicant systems, not job aggregators - so a run answers in seconds and does not fall over when Indeed changes its markup for the third time this month.

### What most job scrapers do, and why this is different

| The usual offer | Here |
|---|---|
| Scrapes one site; LinkedIn or Indeed blocks it and the run returns nothing | Reads six ATS APIs directly - Greenhouse, Ashby, Lever, Workable, SmartRecruiters, Recruitee - and detects which one a company uses from its domain |
| Silent failure: no rows, no reason | Every company is reported: the ATS it uses, jobs on the board, jobs delivered, or the exact reason it could not be resolved (`COMPANY_REPORT` in run storage) |
| You pay per run, even when it returns an empty list | You pay per job delivered. A company with no open roles costs nothing |
| Descriptions, salary and remote flag arrive inconsistently | One flat schema for all six sources; salary parsed to min/max/currency/period |
| Needs residential proxies, billed to you | Zero proxy traffic - the sources are public JSON |
| Same list checked hourly = every board hit hourly, forever | Answers are cached in your own storage for 60 minutes (`cacheMinutes`): a repeat check inside that window is instant and sends no duplicate request |

### How it finds the board

A domain almost never matches the board slug, so detection tries, in order:

1. **The careers page.** `example.com/careers` and friends are read, and any ATS link on them is followed (`boards.greenhouse.io/acme`, `jobs.ashbyhq.com/acme`, and so on).
2. **The domain itself** as a slug against all six APIs.
3. If nothing answers, the company is marked `NO_ATS_FOUND` with a hint - pass the board slug (`leverdemo`) or the full careers URL instead of guessing.
   If the careers page names a provider this actor cannot read (personio today), the row says `UNSUPPORTED_ATS` and names it, so a found board is never reported as missing.

You can also pass a board slug or a careers URL directly; both are understood.

### Input

```json
{
  "companies": ["stripe.com", "figma.com", "notion.so", "canva.com"],
  "keywords": "engineer, data",
  "location": "remote",
  "onlyRemote": false,
  "includeDescription": false,
  "maxJobsPerCompany": 0,
  "maxCompanies": 50,
  "cacheMinutes": 60
}
```

- `keywords` filters on the job title, case-insensitive, comma separated. Filtered-out jobs are not charged.
- `location` is a substring match on the job location.
- `includeDescription` adds the full description as plain text; the price per job does not change.
- `cacheMinutes` reuses a result from your own key-value store within that window. `0` = always live. A company we could not resolve is cached for half the window, so a board that was briefly down recovers on the next run.

### Output

One row per job:

```json
{
  "company": "Stripe",
  "domain": "stripe.com",
  "ats": "greenhouse",
  "jobId": "4567890",
  "jobTitle": "Software Engineer, Payments",
  "department": "Engineering",
  "location": "London, UK",
  "remote": false,
  "employmentType": null,
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryPeriod": null,
  "postedAt": "2026-10-28T09:12:41-04:00",
  "url": "https://stripe.com/jobs/search?gh_jid=4567890",
  "status": "FOUND",
  "fromCache": false,
  "fetchedAt": "2026-11-04T15:30:00+00:00"
}
```

`fromCache: true` means the row was served from your cache - same data, no extra request to the board. The run summary also reports `cacheHits` and `cacheMisses`, so you can see exactly what was reused.

### Who uses this

- **Recruiters and sourcing teams** - a target-company list becomes a live vacancy list.
- **B2B sales** - a company that is hiring three sales reps has budget; that is the strongest intent signal there is.
- **Job boards and aggregators** - fill categories from company boards instead of scraping aggregators.
- **Market research** - salary ranges and remote share by company, straight from the source.

### Price

**$0.0015 per delivered job** ($1.50 per 1,000). A run that finds nothing costs nothing. Extraction of the description costs the same as the row itself.

The platform puts a default spending cap of about **$4.63** on a run when the buyer does not set one, which is roughly 3,000 delivered jobs: a list of 50 companies on large boards reaches it. That cap is the buyer's setting, not ours - raise `maxTotalChargeUsd` in the run options when the whole list must be covered. When it is hit, the run stops and every company it never reached comes back as `NOT_CHECKED_CAP` with the reason.

### Cost of a run

The actor only makes HTTP calls to public JSON endpoints: a 50-company run typically takes well under a minute on the smallest memory setting. With the cache on, a repeat check of an unchanged list makes almost no requests at all.

### Notes

- Only boards the company published publicly are read; nothing is logged in to.
- Salary is returned when the ATS exposes it (Ashby and Lever usually do; Greenhouse usually does not).
- Workday, SuccessFactors and Taleo boards are not covered yet - tell us if your list needs them.
- `maxJobsPerCompany` is a per-company safety stop. When it keeps jobs back, the summary reports
  `jobsOverMaxJobsPerCompany`, so a capped board is never presented as a board that small.
- Two input rows can resolve to the same board (`stripe.com` and `https://stripe.com/careers`): the second
  answer is not thrown away. It goes through the same job-id gate, so a job already delivered is skipped and a
  job only the second answer carried is delivered and charged once. When it brings nothing new it comes back as
  `DUPLICATE_BOARD` with the name of the row that carries the jobs, and nothing is charged twice. Those
  extra jobs are counted as `jobsFromDuplicateAnswers`, because `jobsMatched` counts a board once.
- A company whose jobs the spending cap interrupted mid-way is marked `partial: true` in `COMPANY_REPORT`
  with the counts it did deliver - half a board is never reported as a whole one.
- `maxCompanies` defaults to 50: a longer list is cut at the input, and the companies beyond the limit come
  back as `NOT_CHECKED_MAX_COMPANIES` records with the reason (rows in the dataset when `includeEmptyCompanies`
  is true). `companiesAsked` counts distinct companies and `companiesOverMaxCompanies` counts what the cut took,
  so a 100-company list never reads as a 50-company answer. Case-insensitive repeats of one input
  (`a.com`, `A.COM`) collapse into one company and `companiesDeduplicated` says how many lines that was.
- If the run stops at your spending limit, the companies it never reached are still in `COMPANY_REPORT`
  with `status: NOT_CHECKED_CAP` and a reason, and the summary counts them as `companiesNotChecked`.
  A company that was not checked is never presented as a company with no open jobs.
- If a charge call fails twice in a row, the run stops instead of handing out the rest of the list for free:
  the summary says `billingActive: false` with `stopReason: billing-error`, and the companies it never reached
  come back as `NOT_CHECKED_BILLING`. The row that was already delivered stays delivered and stays
  readable, but it is not paid for: rows handed out while billing was failing are free to the buyer.
- If the platform reports that this run is not pay-per-event (for example the price is not set up
  yet), nothing is charged at all: the rows are still delivered, and the summary says
  `billingActive: false` with `stopReason: billing-not-active`. A buyer is never told he paid
  for rows that came out free.

### Cache and billing

A row served from the cache is charged like any other row: you pay for delivered jobs, not for
network calls. What the cache buys you is time - a repeated company resolves in seconds instead of
probing six ATS APIs again.

# Actor input Schema

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

One line per company: a domain (stripe.com), a careers URL (https://jobs.lever.co/leverdemo) or any ATS board slug. Every company is resolved on its own and reported back, so a company with no open roles is visible in the run report instead of vanishing.

## `keywords` (type: `string`):

Only jobs whose title contains any of these words, comma separated - e.g. engineer, sales, python. Empty = every open job. You are not charged for jobs the filter drops.

## `location` (type: `string`):

Case-insensitive substring of the job location, e.g. London, Berlin, US. Empty = all locations.

## `onlyRemote` (type: `boolean`):

true = keep only jobs the company itself marks as remote.

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

true = add the complete job description as plain text to every row. Rows become much larger; the price per job does not change.

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

Safety stop per company. 0 = every matching job on the board. When the cap keeps jobs back, the number is reported as jobsOverMaxJobsPerCompany in the summary, so a capped board never reads as a complete one.

## `maxCompanies` (type: `integer`):

Safety stop: the run never touches more than this many companies. Default 50. A longer list is cut here, and every company beyond the limit is reported as NOT_CHECKED_MAX_COMPANIES with the reason (and becomes a dataset row when includeEmptyCompanies is true). The summary reports companiesAsked as the number of distinct companies your list named (case-insensitive repeats collapse, companiesDeduplicated says how many input lines that merged), and companiesOverMaxCompanies as how many of them the limit kept out.

## `includeEmptyCompanies` (type: `boolean`):

false = companies without jobs are listed in the run report (storage key COMPANY_REPORT) only. true = every one of them also becomes a dataset row, so nothing is missed downstream: NO_JOBS (board read, nothing matched), FAILED (board not resolved, with the reason), NOT_CHECKED_CAP and NOT_CHECKED_MAX_COMPANIES (never read, with the reason), DUPLICATE_BOARD (same board as another input row).

## `cacheMinutes` (type: `integer`):

Checking the same company again inside this window is served from this account's own cache instead of hitting the board once more: faster runs, and the source sees no duplicate traffic. 0 = always fetch live. A company we could not resolve is cached for half the window only, so a board that was briefly down recovers quickly.

## Actor input object example

```json
{
  "companies": [
    "linear.app",
    "monzo.com"
  ],
  "keywords": "",
  "location": "",
  "onlyRemote": false,
  "includeDescription": false,
  "maxJobsPerCompany": 0,
  "maxCompanies": 50,
  "includeEmptyCompanies": false,
  "cacheMinutes": 60
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset with one row per open job: company, ats, title, department, location, remote, salary, postedAt, apply URL.

## `report` (type: `string`):

Per company: which ATS it uses, how many jobs its board holds, how many were delivered, and the reason when it could not be resolved (FAILED), was never checked (NOT_CHECKED_CAP, NOT_CHECKED_MAX_COMPANIES, NOT_CHECKED_BILLING), was a duplicate board (DUPLICATE_BOARD), or was only partially processed. This is the full list of everything you sent.

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

Counters for the whole run: companiesAsked (distinct companies you sent), companiesDeduplicated (input lines collapsed into those), companiesProcessed, companiesResolved, companiesFailed, companiesWithNoJobs, companiesNotChecked, companiesOverMaxCompanies, companiesPartiallyProcessed, companiesDuplicateBoards, jobsDelivered, jobsMatched, jobsOverMaxJobsPerCompany, jobsFromDuplicateAnswers (jobs only a second input row for the same board carried, which is what makes jobsDelivered exceed jobsMatched), stopped and stopReason (spending-cap, billing-error or billing-not-active), billingActive, cache hits.

# 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": [
        "linear.app",
        "monzo.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("stas_k/company-jobs-api").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": [
        "linear.app",
        "monzo.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("stas_k/company-jobs-api").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": [
    "linear.app",
    "monzo.com"
  ]
}' |
apify call stas_k/company-jobs-api --silent --output-dataset

```

## MCP server setup

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

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/JfScsAVmXb5VnofgJ/builds/G2o4sdCemYLluzQmL/openapi.json
