# Company Jobs & Salary Scraper (`eiv/job-postings-scraper`) Actor

Every open role at any company, straight from the job board it publishes on — Greenhouse, Lever, Ashby or SmartRecruiters. Salary ranges included where they exist, title, department, location, remote flag and posting date. Give it a company name; it finds the board.

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

## Pricing

from $1.20 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Company Jobs & Salary Scraper

Every open role at any company, read straight from the job board it publishes on — **Greenhouse, Lever, Ashby or SmartRecruiters** — with the salary range wherever one exists.

```
openai       734 jobs   85% priced   median $234,000   86 posted this week   471 remote
databricks   800 jobs   53% priced   median $182,000   38 posted this week   207 remote
palantir     318 jobs   74% priced   median $126,000   Lever + SmartRecruiters
ramp         136 jobs   98% priced   median $167,300   124 remote
stripe       568 jobs    3% priced   median $158,400

Staff Software Engineer, Forward Deployed AI    $189,000 – $330,000    Remote
Design Engineer                                 $172,000 – $440,000    New York
```

**2,590 roles from 7 companies in 17 seconds.** No API key, no login, no browser. These are the JSON endpoints the platforms publish so job boards can consume them.

***

### Give it a name, not a platform

You do not need to know which system a company uses. Type `stripe` and all four boards are searched; whichever list the company answers on is the one you get.

Paste the careers URL if that is easier — `https://boards.greenhouse.io/stripe` reduces to `stripe`. Write `greenhouse:stripe` to pin one and skip the search.

**A company can be on two at once.** Palantir has 308 roles on Lever and 10 on SmartRecruiters. Both are jobs someone could apply for, so both are returned, and `platformsFound` says where each came from.

***

### Salary, which is the part nobody else gives you

**1,430 of 2,590 postings in one run carried a pay figure.** Coverage depends entirely on the platform:

| Platform | Pay coverage | How it is carried |
|---|---|---|
| **Ashby** | 85–98% | A structured field. OpenAI 621/734, Ramp 130/136 |
| **Lever** | ~75% | Stated in the description text |
| **Greenhouse** | 3–53% | Description only, and most boards say nothing |

Every figure is normalised: `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, plus `annualisedMin` / `annualisedMax` so an hourly rate and a yearly one sort on one scale. `salaryRaw` keeps the exact text it was read from, so a surprising number can be checked against its source, and `salarySource` says whether it came from a field or from prose.

***

### What you get

**Per role** — `title`, `department`, `team`, `location` and **every other location it is open in**, `isRemote`, `workplaceType`, `country`, `employmentType`, the full salary block, `hasEquity`, `postedAt`, **`daysSincePosted`**, `jobUrl`, `applyUrl`, `jobId`, and the description on request.

**Per company** — which platforms it was found on, jobs returned, pay coverage, median salary and its currency, remote count, **postings in the last 7 and 30 days**, top departments, top locations, countries, newest posting.

Three ready-made views: **Open roles**, **Pay data**, **Company summary**.

***

### Who this is for

- **Sales and GTM** — hiring is a buying signal. Five new AE openings means a team expanding; `postedLast7Days` and `topDepartments` are that signal.
- **Recruiters and job boards** — a clean feed with stable `jobId`s to diff on.
- **Compensation benchmarking** — real ranges by title, company and location, from the source rather than self-reported.
- **Candidates and career tools** — filter by remote, department and a salary floor across every company you care about.

***

### Input

```json
{
  "companies": ["stripe", "databricks", "openai", "ramp"],
  "keywords": ["engineer"],
  "excludeKeywords": ["intern", "manager"],
  "remoteOnly": true,
  "requireSalary": true,
  "minSalary": 180000
}
```

That run returns 115 roles out of 2,238 — every one remote, priced, and paying over the floor.

| Option | Default | Notes |
|---|---|---|
| `companies` | — | Token or careers URL. `greenhouse:stripe` pins a platform |
| `platforms` | all four | Narrow it if you already know where a company posts |
| `keywords` / `excludeKeywords` | — | Title, department, team, and description when included |
| `departments` | — | Substring: `Engineering` catches `Engineering - Pipeline` |
| `locations` | — | Matches any location a role lists; `remote` matches the flag |
| `remoteOnly` | `false` | |
| `postedWithinDays` | — | Undated postings are kept |
| `minSalary` | — | Against the top of the range; unpriced roles kept |
| `requireSalary` | `false` | Drops unpriced roles instead |
| `includeDescription` | `false` | Most of the payload. Pay is read either way |
| `maxJobsPerCompany` | `1000` | |

***

### Six things worth knowing

Each was found by running against live boards.

**A company's board can be found without knowing its platform, and the search is cheap.** All four are asked at once and a fake name cleanly finds nothing. What it cannot do is find a company on Workday or Taleo, which between them hold most large-enterprise hiring and publish nothing readable. Visa returns 2 roles because that is genuinely all it lists on SmartRecruiters; the rest are on Workday. A summary row with `errorCode: NO_BOARD` means the token was not found, not that nobody is hiring — those two are never blurred.

**Choosing one board per company was wrong twice over.** Palantir came back with 10 roles when it has 308, because the router picked whichever platform reported more and the probes were not measuring the same thing — Lever has no count endpoint, so asking for one posting reported a board of 308 as holding one, while SmartRecruiters reports its true total regardless. Rather than fix the comparison, the comparison is gone: every board that answers is read and merged.

**Greenhouse hides pay in prose, and asking for jobs without prose used to discard it.** The description was only fetched when the caller wanted descriptions in the output, which silently threw away the pay for 1,368 postings in one run. It is now always fetched and only dropped from the output, taking coverage from 9% to 55%.

**A job description is full of money that is not salary.** Greenhouse gave up `$500M` — the funding round. Lever gave up `contract values in excess of $0.5M`. Both are plausible annual figures. Millions are refused outright, because salaries are written `$257K` and never `$0.257M`.

**Benefits did more damage than funding ever did.** *"Competitive compensation, 100% employer match on 401k contributions up to 10% (cap at $10,000)"* names compensation, sits beside the pay talk, and reads as a $10,000 salary. It mispriced **41 of 80 roles** at one company. Benefits vocabulary is now excluded before pay context is even considered.

**Greenhouse escapes its descriptions twice.** A pay range is three spans with `&mdash;` between them, and the whole thing is entity-encoded again on the way out, so the separator arrives as `&amp;mdash;`. Decode once and the literal text `mdash;` survives, which is not a dash, so no range ever matches and the single-value fallback takes the bottom of it — **427 of 800 Databricks salaries understated by the full width of their own range.** Decoding twice turned 0 ranges into 423.

***

### Output

```json
{
  "recordType": "job",
  "company": "Databricks",
  "platform": "greenhouse",
  "title": "Senior Software Engineer - Data Platform",
  "department": "Engineering",
  "location": "San Francisco, CA",
  "allLocations": ["San Francisco, CA", "Seattle, WA"],
  "isRemote": false,
  "salaryMin": 145700, "salaryMax": 200300,
  "salaryCurrency": "USD", "salaryPeriod": "year",
  "annualisedMin": 145700, "annualisedMax": 200300,
  "hasEquity": true,
  "salaryRaw": "$145,700 — $200,300",
  "salarySource": "description",
  "daysSincePosted": 3,
  "jobUrl": "https://boards.greenhouse.io/databricks/jobs/..."
}
```

Set `flattenOutput: true` for CSV.

***

### Honest limits

- **Four platforms, not the whole market.** Greenhouse, Lever, Ashby and SmartRecruiters publish openly. Workday and Taleo do not, and Teamtailor wants an API key. A company on those returns `NO_BOARD`.
- **Most Greenhouse boards publish no pay at all.** Stripe states a figure on 16 of 568 postings. That is Stripe's choice, not a parsing failure, and `salaryCoverage` reports it per company so you can see which is which.
- **A salary read from prose is a reading.** `salarySource` tells you which figures came from a field and which from text, and `salaryRaw` shows the exact string, so anything surprising can be checked in one glance.
- **Annualising assumes 2,080 hours a year**, twelve months, fifty-two weeks. It is only ever derived from a period the posting actually stated.
- **Ashby publishes no company name**, so those rows carry the token you supplied rather than a display name. Greenhouse, Lever and SmartRecruiters give a real one.
- **`postedAt` is often missing on SmartRecruiters.** Undated postings are kept when filtering on age, because an unknown date is not evidence a role is stale.
- **Companies not found, postings your filters removed, and postings already returned by another entry in your list are never charged.**

***

### Pricing

| Event | Price | When |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Company searched | $0.006 | Per company found and read, including its summary |
| Job found | $0.0012 | Per open role returned |
| Salary found | $0.002 | Only when a pay figure was established |

**About $2.30 per 1,000 roles** on a board where half publish pay. The 2,590-role run above costs **$6.02**. A daily watch on twenty companies is a few cents.

LinkedIn and Indeed scrapers charge more, fight anti-bot, and hand you no salary.

***

### Tips

- **Run daily and diff on `jobId`.** New ids are new openings; ids that vanish are roles filled or pulled.
- **Watch `postedLast7Days` per company.** That is the hiring-signal number — a jump means a team is scaling.
- **`topDepartments` tells you what kind of growth it is.** Five open sales roles and no engineers means something very different from the reverse.
- **Sort on `annualisedMax`** to compare pay across hourly, monthly and yearly postings honestly.
- **Leave `includeDescription` off** unless you need the text. It is most of the payload and costs you no pay data.

# Actor input Schema

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

The token each company's job board is published under — the last part of its careers URL. boards.greenhouse.io/stripe is 'stripe', jobs.lever.co/palantir is 'palantir', jobs.ashbyhq.com/ramp is 'ramp'. Paste the whole URL if you prefer; it is reduced for you. All four platforms are searched, so you do not need to know which one a company uses. Write greenhouse:stripe to pin one and skip the search.

## `platforms` (type: `array`):

Leave empty for all four. greenhouse, lever, ashby, smartrecruiters. Narrowing this makes the search cheaper when you already know where a company posts.

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

Any one matching is enough. Searched in the title, department and team, and in the description when descriptions are included.

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

Drop any posting matching one of these. Useful for stripping 'Intern', 'Contract' or a product line you do not care about.

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

Matched against the department and the team, as a substring. 'Engineering' catches 'Engineering - Pipeline'.

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

Matched against every location a posting lists, not only the first — a role open in four cities matches a search for any of them. The word 'remote' also matches a posting flagged remote whose location does not say so.

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

Keep only postings the platform flags as remote, or whose location says so.

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

Drop anything older. Postings with no date are kept — SmartRecruiters often omits it, and an unknown date is not evidence a role is stale.

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

Compared against the top of the annualised range, so a role paying 150k-190k clears a 180k floor. Annualising means an hourly rate and a yearly one are judged on the same scale. Postings that publish no pay are kept, because most do not and dropping them silently would hide the majority of the market. Use 'Only postings that publish pay' to drop them instead.

## `requireSalary` (type: `boolean`):

Keep only postings whose salary could be established. Expect this to narrow a run sharply on Greenhouse boards, which mostly state nothing.

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

Adds the posting text to every record. This is most of the payload — one board goes from 139 KB to 4.2 MB — so leave it off unless you need it. Salary is read from the description either way; turning this off does not cost you the pay data.

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

A large board runs to several hundred roles. The summary row carries totalAvailable where the platform reports one, so you can see what was left behind.

## `includeCompanySummary` (type: `boolean`):

Add one rollup per company: which platforms it was found on, pay coverage, median salary and its currency, remote count, postings in the last 7 and 30 days, top departments and locations. Not billed as a job.

## `flattenOutput` (type: `boolean`):

Emit flat dot-notation columns. Mainly affects the list of locations and the top-department list on summary rows.

## `maxConcurrency` (type: `integer`):

Companies read in parallel. Each one searches up to four platforms, so this multiplies. Five is comfortable; the boards will rate limit above that.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. A large board with descriptions is several megabytes.

## `maxRetries` (type: `integer`):

Retries for connection resets and 5xx responses. A 429 backs off in seconds rather than milliseconds, and honours Retry-After.

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

Optional. All four are public job board APIs and none of them blocks.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "ramp",
    "databricks"
  ],
  "remoteOnly": false,
  "requireSalary": false,
  "includeDescription": false,
  "maxJobsPerCompany": 1000,
  "includeCompanySummary": true,
  "flattenOutput": false,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 60,
  "maxRetries": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Job records carry recordType 'job'; rollups carry 'company-summary'.

# 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",
        "ramp",
        "databricks"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/job-postings-scraper").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",
        "ramp",
        "databricks",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("eiv/job-postings-scraper").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",
    "ramp",
    "databricks"
  ]
}' |
apify call eiv/job-postings-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/zcdc4WP3jRRJgHzwP/builds/uPTa04k8gtFwRjfEv/openapi.json
