# Career Site Jobs Scraper - $0.55/1k, 10 ATS (`dami_studio/career-site-jobs-scraper`) Actor

Paste a company career page URL or company name and get every open job: title, location, department, employment type, posted date, apply link and the ATS detected for you. 10 ATS families. $0.55 per 1,000 jobs, flat on every plan — the cheapest career-site job scraper on the market.

- **URL**: https://apify.com/dami\_studio/career-site-jobs-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.55 / 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 Jobs Scraper

Give it a company career page — or just a company name — and get back every open job on it: title, location, department, employment type, posted date, a direct apply link, and which applicant tracking system the page is actually running on. It reads the public job-board feed that the company's own careers widget reads, across Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, Breezy HR and Workday. No account, no cookies, no login, no browser rendering.

- Paste a careers URL, a bare domain, a board URL you already know, or a plain company name — all four work.
- The applicant tracking system is detected for you and reported on every row, so you never have to look it up first.
- Ten ATS families supported, covering the large majority of company career pages that publish an open job feed.
- Many companies resolve in a single request, because the board is identified before anything heavy is fetched.
- Runs with empty input return a labelled sample row, free, so you can see the shape first.
- Jobs are deduplicated per company, so the same posting is never charged twice in one run.

### Price

**$0.55 per 1,000 jobs**, plus a **$0.001 start fee per run** (billed per gigabyte of run memory, so exactly that on the default 1 GB).

This is **the cheapest company career page jobs scraper on the market**, and it is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

| Jobs | Total cost |
|---|---|
| 100 | $0.056 |
| 1,000 | $0.551 |
| 10,000 | $5.501 |
| 100,000 | $55.001 |

#### What is actually charged

- **One `job-scraped` event per job row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- Companies whose career page does not expose a supported job feed cost you nothing — they produce an uncharged diagnostic row, not a billed one.
- Companies whose board exists but currently lists no open roles are free for the same reason.
- Duplicate postings inside one company are dropped before they are charged.
- Everything the run does to work out which system a company uses is free. You are billed for jobs, not for lookups.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to `apify-default-dataset-item` — the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "companies": [
    "https://www.figma.com/careers",
    "https://jobs.ashbyhq.com/ramp",
    "notion.com",
    "Databricks"
  ],
  "maxItems": 100
}
```

| Field | What it does |
|---|---|
| `companies` | One entry per company, up to 50 per run. A careers page URL, a bare domain, a board URL, or a company name. The run works out the rest. |
| `maxItems` | Total jobs across all companies. The budget is split evenly, so five companies and 50 jobs gives you ten each. Default 50, hard ceiling 5,000. Keep it low while testing — you pay per job. |
| `fullDetails` | Off by default. Adds a plain-text `description` to every row, and fills in the posted date on Ashby rows. Slower and much heavier, because description feeds are many times larger than listing feeds. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per job. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "job",
  "inputUrl": "https://www.figma.com/careers",
  "company": "figma",
  "ats": "greenhouse",
  "atsName": "Greenhouse",
  "boardToken": "figma",
  "title": "Software Engineer, Design Systems",
  "location": "San Francisco, CA • New York, NY",
  "remote": null,
  "department": "Engineering",
  "employmentType": null,
  "postedAt": "2026-06-30T18:11:04.000Z",
  "applyUrl": "https://job-boards.greenhouse.io/figma/jobs/5561234004",
  "jobUrl": "https://job-boards.greenhouse.io/figma/jobs/5561234004",
  "jobId": "5561234004",
  "salary": null,
  "detectedVia": "token",
  "scrapedAt": "2026-08-15T16:42:08.911Z"
}
```

#### Field notes

- `inputUrl` — the exact string you supplied for this company, so you can join the rows back onto your input list.
- `ats` — the applicant tracking system detected, lower-case and stable: `greenhouse`, `lever`, `ashby`, `workable`, `smartrecruiters`, `recruitee`, `personio`, `teamtailor`, `breezy`, `workday`. `atsName` is the same thing spelled for humans.
- `boardToken` — the company's identifier on that system. Handy if you want to hit the same board again later without re-detecting it.
- `location` — as the company wrote it. Some boards give one city, some give a list, some give a region like "Europe". Multiple locations are joined with commas.
- `department` — the team or department the board files the job under. A few systems do not expose it — see Limitations.
- `employmentType` — full-time, part-time, contract, intern and so on, in the wording the board uses. Null where the board does not record it.
- `postedAt` — when the posting was published, normalised to ISO 8601 in UTC. Null where the board does not publish a date.
- `applyUrl` — the page that starts an application. On some systems that is the posting page itself.
- `jobUrl` — the public posting page, suitable for sharing.
- `jobId` — the posting's id on its board. Stable, and the right key to diff on when you re-run.
- `remote` — true or false where the board records it explicitly, otherwise inferred from the location text, otherwise null.
- `salary` — only where the board publishes a range. Most do not.
- `detectedVia` — how the board was found: `url` (your input already named it), `token` (matched from the company domain or name), or `html` (read off the careers page).

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- Nearly every company careers page is a thin front end over an applicant tracking system, and those systems publish an unauthenticated JSON feed of open roles so the company's own page can render them. This Actor reads that feed directly.
- Working out which system a company uses is the real work, and it is done cheapest-first. If the URL you gave already names a board, that is free. Otherwise the run derives a likely board identifier from the domain or company name and asks the boards directly — a miss is a tiny 404, far cheaper than downloading a corporate web page.
- Only if both of those fail does it fetch the careers page itself and look for an embedded board. There is no page rendering anywhere in the chain, because the pages in question are themselves just calling the feeds we already read.
- Each feed is requested in its lightest documented form. Where a system offers both a listing feed and a much larger feed with every job description inlined, the listing feed is used unless you ask for descriptions.
- Requests leave through a large pool of rotating datacenter addresses, so a per-address rate limit is answered by moving rather than by waiting.

### What people use it for

- Watching a list of target companies for new openings: run the same list on a schedule and diff on `jobId` to see only what appeared since last time.
- Building a niche job board or newsletter from a curated set of employers, without signing up to an aggregator feed.
- Recruiting and talent mapping — see which departments a chosen set of employers is growing, and where.
- Sales and hiring-signal research: a burst of postings in one function is a strong buying signal, and `department` plus `postedAt` gives you that directly.
- Market research on remote policy, employment types or salary disclosure across an industry, using flat typed columns instead of scraped page text.
- Enriching a CRM or spreadsheet of companies with a live open-roles count and the system each one hires through.

### What "the ATS is detected for you" means in practice

Most tools in this space make you supply the applicant tracking system and the board identifier yourself — you have to already know that Figma hires through Greenhouse under the token `figma`, and that Ramp uses Ashby. That is fine for three companies and unusable for three hundred.

Here you give the thing you actually have — a careers URL off the company website, a domain out of your CRM, or a company name out of a spreadsheet — and the run resolves it. Every row comes back stamped with the `ats` and `boardToken` it resolved to, so the first run doubles as a way to build that mapping for yourself.

Detection is cheapest-first and never renders a page, so on the common case it costs a single small request before any job data is read.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "job"`. These are the rows you paid for, one billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no companies in it, and it exists so you can look at the output shape before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NO_RESULTS` when a company has no supported feed or no open roles, `NOT_FOUND` when a board id turned out not to exist, `RATE_LIMITED` when a board throttled the run, `SERVER_ERROR` when a board was briefly down, `NETWORK` when it could not be reached at all, and `TIME_BUDGET` when the run ran out of time before reaching a company. Each one carries the `inputUrl` it belongs to and a plain-English explanation.

If you only want the data, filter on `charged == true`. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

### Getting the best coverage

If a company comes back as `NO_RESULTS` and you believe it does publish jobs, the fastest fix is to give the board URL directly. Open the company careers page, click through to any single job, and look at the address bar — if it lands on a job-board host, paste that URL in instead of the marketing page and the run will use it verbatim.

Workday boards must always be supplied as a URL, because a Workday address contains a tenant, a datacenter and a site name that cannot be guessed from a company name. Any Workday careers URL works — the run pulls the three parts out of it.

### Limitations

- Only career pages running on Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, Breezy HR and Workday are supported. Large enterprises frequently use systems that publish no open job feed at all; those companies return an uncharged diagnostic row rather than data.
- A company is only found if its board identifier can be derived from its domain or name, or is present on its careers page, or you supplied the board URL yourself. A company whose board id bears no relation to its name and who does not embed the board on a fetchable page will not resolve.
- Workday needs a full careers URL, not a company name — the address contains a tenant and site id that cannot be guessed.
- Posted dates are not universal. Ashby only exposes them on the heavier feed, so they arrive null unless you turn on full details; Personio does not publish them at all; Workday reports them as relative text ("Posted 30+ Days Ago") which is converted to an approximate date, not an exact one.
- Department is not universal either. Workday and Teamtailor listing feeds do not carry it at all, SmartRecruiters carries it on roughly a third of postings, and Personio boards that do not categorise their roles leave it empty.
- Employment type is null on Greenhouse and Workday, which do not carry it in their public listing feeds.
- Salary appears only where the board publishes a range. The large majority do not.
- Job descriptions are off by default. Turning them on makes runs slower and much heavier, and the text is flattened to plain text with the HTML stripped.
- Results are the boards' current open roles. There is no history and no closed-job archive; a posting that was filled yesterday is simply absent today.
- Boards rate-limit by address under heavy use. The run rotates addresses and retries, but a very large company list in a single run can still hit a throttle and return diagnostic rows for the tail.
- The time spent working out which system a company uses is paid for by the jobs the run finds, so a run that finds nothing gives up quickly: if the first companies in your list cannot be resolved, the rest come back as uncharged `TIME_BUDGET` rows within a few seconds rather than the run grinding through all fifty. That keeps a list of dead URLs from eating your run time, and it never happens once jobs start coming back. Putting a board URL first, or supplying board URLs throughout, sidesteps it entirely — a board URL needs no lookup at all.
- Location is whatever the board recorded, which is not normalised across systems — one company writes "Remote - US", another writes "San Francisco, California, United States".
- The hard ceilings are 50 companies and 5,000 jobs per run. Split larger jobs across runs.

### Questions

**What do I actually paste in?**

Whatever you have. A careers page URL, a bare domain like `figma.com`, a board URL like `https://jobs.lever.co/matchgroup`, or just `Figma`. All four resolve to the same board. A URL is more reliable than a name, and a board URL is the most reliable of all.

**How do I know which system a company uses?**

You do not need to — that is the point. Every row tells you, in the `ats` and `boardToken` fields. Run your list once and you have the mapping for good.

**What happens if a company is not supported?**

You get one uncharged diagnostic row for it with `errorCode: "NO_RESULTS"` explaining what was tried, and the run carries on to your other companies. You are never billed for a company that returned no jobs.

**Why is `postedAt` sometimes null?**

Because the board does not publish it. Different systems expose different fields, and inventing a date would be worse than leaving it empty. The Limitations section lists exactly which systems omit it.

**Can I get the full job description?**

Yes — turn on full details. It is off by default because the description feeds are many times larger than the listing feeds, so leaving it on makes every run slower for data most callers do not use.

**Do I need a proxy?**

No. The run brings its own egress and the cost of it is already inside the price you see. The `proxyUrls` field exists only for callers who specifically want traffic to leave through servers they already own.

**Will the run fail if one company breaks?**

No. A blocked, empty or unsupported company produces an uncharged diagnostic row and the run carries on and still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

**Can I run this on a schedule?**

Yes. Nothing in the run holds state between runs, so a schedule with the same company list is safe. Diff on `jobId` to see which postings are new since last time.

**How do I get exactly the rows I paid for?**

Filter the dataset on `"charged": true`. Sample and diagnostic rows are always `false`, and the number of charged rows always equals the number of billed events.

# Actor input Schema

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

One entry per company, up to 50 per run. Any of these work: a careers page (https://www.figma.com/careers), a bare domain (figma.com), a board URL you already know (https://boards.greenhouse.io/figma, https://jobs.lever.co/matchgroup, https://jobs.ashbyhq.com/ramp, https://apply.workable.com/skroutz), or just the company name (Figma). The Actor works out which applicant tracking system the company uses and reads that system's public job board.

## `maxItems` (type: `integer`):

Total jobs to return across all companies. The budget is shared evenly between them, so five companies and 50 jobs gives you ten each. Keep it low while you are testing - you pay per job.

## `fullDetails` (type: `boolean`):

Off by default. When on, each row gains a plain-text `description` field, and Ashby rows also gain their posted date. It makes the run slower and heavier because the description feeds are many times larger, so leave it off unless you actually need the job text.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "companies": [
    "https://www.figma.com/careers",
    "https://jobs.ashbyhq.com/ramp",
    "notion.com"
  ],
  "maxItems": 50,
  "fullDetails": false
}
```

# Actor output Schema

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

Every row in the default dataset: inputUrl, company, ats, boardToken, title, location, remote, department, employmentType, postedAt, applyUrl, jobUrl, jobId, salary. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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": [
        "https://www.figma.com/careers",
        "https://jobs.ashbyhq.com/ramp",
        "notion.com"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/career-site-jobs-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": [
        "https://www.figma.com/careers",
        "https://jobs.ashbyhq.com/ramp",
        "notion.com",
    ],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/career-site-jobs-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": [
    "https://www.figma.com/careers",
    "https://jobs.ashbyhq.com/ramp",
    "notion.com"
  ],
  "maxItems": 50
}' |
apify call dami_studio/career-site-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/career-site-jobs-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/Lxd9ORbF5N3M9FZQs/builds/1vBJxBsaWWScbYuaN/openapi.json
