# Career Site Job Scraper - Greenhouse, Lever, Ashby + 6 More ATS (`boztek-ltd/career-site-job-scraper`) Actor

Scrape job listings straight from company career pages across 9 applicant tracking systems.

- **URL**: https://apify.com/boztek-ltd/career-site-job-scraper.md
- **Developed by:** [Boztek LTD](https://apify.com/boztek-ltd) (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 $4.00 / 1,000 job scrapeds

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

## Career Site Job Scraper — Greenhouse, Lever, Ashby + 6 More ATS

Scrape job listings **straight from company career pages** across nine applicant tracking systems. Give it a domain and it works out which ATS the company uses on its own. Every job comes back in one normalized schema, no matter which platform it came from.

Jobs appear on a company's own career page **before** they are syndicated to LinkedIn or Indeed — and unlike those sites, every source here is a public API that needs no proxies, no cookies and no login.

### Supported ATS platforms

| ATS | Board token example | Description | Salary | Employment type | Posted date |
|---|---|---|:---:|:---:|:---:|
| **Greenhouse** | `greenhouse:vercel` | ✅ included | — | — | ✅ |
| **Lever** | `lever:leverdemo` | ✅ included | ✅ when set | ✅ | ✅ |
| **Ashby** | `ashby:ramp` | ✅ included | ✅ structured | ✅ | ✅ |
| **Workable** | `workable:blueground` | ✅ included | — | ✅ | ✅ |
| **SmartRecruiters** | `smartrecruiters:Visa` | ➕ extra request | — | ✅ | ✅ |
| **Recruitee** | `recruitee:hygraph` | ✅ included | ✅ structured | ✅ | ✅ |
| **Personio** | `personio:personio` | ✅ included | — | ✅ | ✅ |
| **Breezy HR** | `breezy:breezy` | ❌ not public | ✅ free text | ✅ | ✅ |
| **Rippling** | `rippling:rippling` | ➕ extra request | — | ➕ extra request | ➕ extra request |

✅ comes with the listing · ➕ needs one extra request per job (charged separately, and only when you ask for descriptions) · — the platform does not expose it · ❌ not available at all

### How you name a company

Put any of these in the `companies` list — you can mix them freely:

| Form | Example | Cost |
|---|---|---|
| **Domain** | `stripe.com` | ATS detection charged once |
| **Career page URL** | `https://ramp.com/careers` | ATS detection charged once |
| **Job board URL** | `https://jobs.ashbyhq.com/ramp` | free — read directly from the URL |
| **Explicit `ats:token`** | `greenhouse:vercel` | free — no detection needed |

ATS detection fetches the company's career page and looks for a job board link. It is the convenient path; `ats:token` is the cheap one.

### What does it cost?

Pay-per-event. You are charged for jobs that **survive your filters**, so narrowing by keyword or location genuinely lowers the bill.

| Event | Price | When |
|---|---|---|
| Job scraped | **$0.004** | Per job in the dataset |
| ATS detected | **$0.01** | Per company resolved from a bare domain |
| Description fetched | **$0.001** | Only for SmartRecruiters and Rippling, only with descriptions on |
| Actor start | **$0.00005** | Once per run |

| Scenario | Cost |
|---|---|
| One company, 100 jobs, explicit `ats:token` | ~$0.40 |
| 50 companies by domain, 2,000 jobs | ~$8.50 |
| Daily watch on 20 companies, ~30 new jobs/day | ~$0.12/day |

Filters and the incremental mode are the levers: a daily run with `onlyNewJobs` costs a fraction of a full re-scrape.

### Only the new jobs

Turn on **`onlyNewJobs`** and the Actor remembers every job ID it has already returned, in a named key-value store. Later runs emit only postings it has never seen.

That turns this into a job alert feed: schedule it hourly, point a webhook at the dataset, and you get new roles within the hour — usually before they reach the big job boards. Use different `stateStoreName` values to keep separate schedules independent.

### Output

One flat item per job — it exports cleanly to CSV and Excel:

```json
{
  "jobId": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "ats": "ashby",
  "atsBoardToken": "ramp",
  "companyName": "Ramp",
  "companyDomain": "ramp.com",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "Full-time",
  "seniority": "Senior",
  "locationRaw": "New York, NY (HQ)",
  "city": "New York",
  "region": "NY",
  "country": null,
  "isRemote": true,
  "workplaceType": "Hybrid",
  "locations": ["New York, NY (HQ)", "San Francisco, CA", "Remote (US)"],
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-.../application",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-...",
  "description": "# About Ramp\n\nRamp is building the smart infrastructure...",
  "descriptionFormat": "markdown",
  "scrapedAt": "2026-08-11T19:36:05.412Z",
  "sourceUrl": "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true"
}
```

#### Fields the Actor derives for you

- **`seniority`** — read from the title (`Intern`, `Junior`, `Mid`, `Senior`, `Staff`, `Principal`, `Lead`, `Director`, `VP`, `C-level`). Most specific wins, so *Senior Staff Engineer* resolves to `Staff`.
- **`employmentType`** — one shared vocabulary. Ashby's `FullTime`, Recruitee's `fulltime_permanent`, Rippling's `SALARIED_FT` and SmartRecruiters' `Regular full time salary` all become `Full-time`.
- **`isRemote`** — the ATS flag when there is one, otherwise inferred from the location wording.
- **`salaryPeriod`** — canonical `hour` / `day` / `week` / `month` / `year`, so Ashby's `1 YEAR` and Recruitee's `month` line up.
- **`city` / `region` / `country`** — split out of the free-text location.
- **`description`** — Markdown by default (best for LLM pipelines), or plain text or the original HTML.

### Input options

| Option | Type | Default | Description |
|---|---|---|---|
| `companies` | array | **required** | Domains, career page URLs, job board URLs or `ats:token` entries |
| `keywords` | array | `[]` | Keep jobs whose title contains any of these |
| `excludeKeywords` | array | `[]` | Drop jobs whose title contains any of these |
| `locations` | array | `[]` | Keep jobs matching any of these locations |
| `departments` | array | `[]` | Keep jobs in these departments or teams |
| `remoteOnly` | boolean | `false` | Remote jobs only |
| `postedWithinDays` | integer | `0` | Keep jobs published in the last N days (`0` = off) |
| `onlyNewJobs` | boolean | `false` | Emit only jobs not seen in previous runs |
| `includeDescription` | boolean | `true` | Include the full job description |
| `descriptionFormat` | string | `markdown` | `markdown` / `text` / `html` |
| `maxJobsPerCompany` | integer | `0` | Cap per company (`0` = no limit) |
| `maxTotalJobs` | integer | `0` | Cap across all companies (`0` = no limit) |
| `stateStoreName` | string | `career-site-job-scraper-state` | Key-value store backing `onlyNewJobs` |
| `maxConcurrency` | integer | `8` | Parallel requests against the ATS APIs |
| `proxyConfiguration` | object | off | Optional — not needed for any supported ATS |

### Use cases

1. **Job alert products** — schedule with `onlyNewJobs` and push new roles to Slack, email or a webhook within the hour.
2. **Recruiting and sourcing** — track which teams a list of competitors is hiring for, and where.
3. **Sales and VC signals** — hiring is a growth signal. Watch a portfolio or an ICP list and see who is scaling which function.
4. **Job boards and aggregators** — build a niche board on first-party data instead of re-scraping LinkedIn.
5. **Feeding AI agents** — Markdown descriptions drop straight into RAG pipelines and LLM matching.

### Why not just scrape LinkedIn or Indeed?

| | LinkedIn / Indeed scrapers | **Career Site Job Scraper** |
|---|---|---|
| Data source | Aggregator, re-posted | **Employer's own board** |
| Freshness | After syndication | **At publication** |
| Blocking | Residential proxies, cookies | **None — public APIs** |
| Reliability | Breaks on layout changes | **Versioned JSON APIs** |
| Description | Often truncated | **Full text** |
| Salary | Rarely present | **Structured where published** |

### Limitations

- **Breezy HR descriptions are not available.** Its public feed carries metadata only, and the posting page is a JavaScript app with no JSON or JSON-LD to read. Everything else — title, location, department, employment type, salary, dates — is returned.
- **Greenhouse does not expose employment type** through its job board API, so that field stays empty for Greenhouse boards.
- **Rippling's listing endpoint is sparse.** Posting date and employment type only arrive with the description, so leaving `includeDescription` off means those fields stay empty for Rippling.
- **`postedWithinDays` keeps jobs whose date is unknown.** SmartRecruiters and Rippling only reveal dates on the detail endpoint; dropping every undated job would be worse than including a few. Dates that arrive during enrichment are re-checked against the filter.
- **ATS detection needs a public link.** If a career page renders its job board purely client-side with no ATS URL in the HTML, detection fails — pass an explicit `ats:token` entry instead.
- **Only public postings.** Internal, draft and unlisted roles are excluded, as they are from the ATS APIs themselves.

### Which companies can I scrape?

Any company on one of the nine supported platforms — collectively tens of thousands of employers, heavily weighted toward tech, startups and scale-ups. If you are not sure which ATS a company uses, just pass its domain and let the Actor find out.

# Actor input Schema

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

One entry per company. Accepts any of these forms:

- **Domain** — `stripe.com` (the Actor finds the career page and detects the ATS)
- **Career page URL** — `https://ramp.com/careers`
- **Job board URL** — `https://jobs.ashbyhq.com/ramp`
- **Explicit `ats:token`** — `greenhouse:vercel` (fastest, skips detection)

Supported ATS: `greenhouse`, `lever`, `ashby`, `workable`, `smartrecruiters`, `recruitee`, `personio`, `breezy`, `rippling`.

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

Keep only jobs whose title contains at least one of these (case-insensitive). Leave empty to keep every job.

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

Drop jobs whose title contains any of these (case-insensitive).

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

Keep only jobs whose location matches one of these (case-insensitive substring, e.g. `Berlin`, `United States`, `EMEA`).

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

Keep only jobs in these departments or teams (case-insensitive substring, e.g. `Engineering`, `Sales`).

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

Keep only jobs flagged as remote by the ATS or whose location mentions remote.

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

Keep only jobs published in the last N days. `0` disables the filter.

## `onlyNewJobs` (type: `boolean`):

Remembers every job ID this Actor has returned before (in a named key-value store) and emits only jobs it has never seen. Ideal for scheduled runs that feed alerts. The first run returns everything.

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

Include the full job description. Greenhouse, Lever, Ashby, Recruitee and Personio return it for free; SmartRecruiters, Workable, Breezy and Rippling need one extra request per job, which is charged as a separate event.

## `descriptionFormat` (type: `string`):

`markdown` is the most useful for LLM pipelines, `text` is the smallest, `html` preserves the original markup.

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

Cap the number of jobs returned per company. `0` means no limit.

## `maxTotalJobs` (type: `integer`):

Hard cap across all companies. `0` means no limit.

## `stateStoreName` (type: `string`):

Named key-value store used by **Only jobs not seen in previous runs**. Change it to keep separate histories for separate schedules.

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

Parallel requests against the ATS APIs. Keep it modest to stay a good citizen.

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

Optional. Every supported ATS exposes a public API that works fine from Apify's datacenter IPs, so a proxy is normally unnecessary.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:vercel",
    "ashby:ramp",
    "lever:leverdemo"
  ],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "onlyNewJobs": false,
  "includeDescription": true,
  "descriptionFormat": "markdown",
  "maxJobsPerCompany": 0,
  "maxTotalJobs": 0,
  "stateStoreName": "career-site-job-scraper-state",
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One item per job listing: title, company, department, location, remote flag, employment type, derived seniority, salary when the ATS publishes it, posting date, apply URL and the description.

## `jobsCsv` (type: `string`):

The same listings as a spreadsheet-ready CSV, for handing straight to a recruiter or a sheet.

## `compensation` (type: `string`):

The listings reduced to the pay-related columns. Only Lever, Ashby, Recruitee and Breezy publish compensation, so rows from the other platforms come back blank.

## `runSummary` (type: `string`):

Which company entries resolved to which ATS board, which ones were skipped and why, any per-board failures, and the count of each event charged.

# 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": [
        "greenhouse:vercel",
        "ashby:ramp",
        "lever:leverdemo"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("boztek-ltd/career-site-job-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": [
        "greenhouse:vercel",
        "ashby:ramp",
        "lever:leverdemo",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("boztek-ltd/career-site-job-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": [
    "greenhouse:vercel",
    "ashby:ramp",
    "lever:leverdemo"
  ]
}' |
apify call boztek-ltd/career-site-job-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,boztek-ltd/career-site-job-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/51sbqk3z8bQWZoJ8r/builds/9VjbBaROzPAAieRli/openapi.json
