# Career Site Job Scraper (`garnet_puppet/career-site-job-scraper`) Actor

Turn a list of company websites into their job listings. It auto-detects which hiring platform each company uses - Greenhouse, Lever, Ashby, Workable, SmartRecruiters and 9 more - so you paste domains, not board slugs. Incremental mode returns only new and changed postings.

- **URL**: https://apify.com/garnet\_puppet/career-site-job-scraper.md
- **Developed by:** [Mehmet](https://apify.com/garnet_puppet) (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 $3.50 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

Scrape job listings from any company's career site. Give it a domain like `vercel.com` — it
automatically detects which applicant tracking system that company uses and returns every open
role in one normalized schema. No board slugs, no per-ATS setup.

### What does Career Site Job Scraper do?

It turns a list of company domains into a clean, structured job feed. Point it at 5 companies or
500 — it figures out which of **14 applicant tracking systems** each one runs on, pulls every
open posting, and normalizes the output so every source shares identical field names.

Click **Start** to try it with the default companies — no configuration needed. Then schedule it
to run daily and get only what changed since the last run.

### Sample output

This record is copied verbatim from a real run of this Actor (only `descriptionHtml` and
`descriptionText` are shortened below — everything else, including the null fields, is exactly
what the Actor returned):

```json
{
  "sourceId": "ashby",
  "companySlug": "ramp",
  "companyName": null,
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "descriptionHtml": "<h1><strong>About Ramp</strong></h1><p>Ramp is building the smart infrastructure for finance teams, embedded in the transaction flow of every dollar a business spends...</p>",
  "descriptionText": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams, embedded in the transaction flow of every dollar a business spends. We automate how over $200B in annualized spend flows in and out of 70,000+ companies...",
  "locationRaw": "New York, NY (HQ)",
  "locations": [
    { "raw": "New York, NY (HQ)", "city": null, "region": null, "country": null },
    { "raw": "Remote (Canada)", "city": null, "region": null, "country": null },
    { "raw": "Remote (US)", "city": null, "region": null, "country": null },
    { "raw": "Miami, FL", "city": null, "region": null, "country": null }
  ],
  "workplaceType": "hybrid",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "FullTime",
  "seniority": null,
  "compensation": {
    "currency": "USD",
    "minAmount": 211400,
    "maxAmount": 290600,
    "interval": "year",
    "raw": "$211.4K – $290.6K • Offers Equity"
  },
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "publishedAt": "2026-04-07T17:12:35.753Z",
  "updatedAt": null,
  "requisitionId": null,
  "scrapedAt": "2026-08-09T19:02:29.589Z",
  "contentHash": "bc292d54781897269d7a38c2bf2efc4b621b51977c70e0d138cd6b20847a10ff"
}
```

Every record has the same shape regardless of which ATS it came from. Some fields are `null` by
design rather than by error — for example Ashby's public API doesn't expose a company name or
requisition ID, so those stay `null` for Ashby records but are populated for sources that provide
them. `compensation` is structured (not just a text string) when the source publishes salary
data — Ashby is the most common source for this.

### How to use Career Site Job Scraper

1. Click **Start** to run it with the sample companies, or replace them with your own.
2. Paste the company websites you care about into **Company domains** — one per line, plain
   domains like `vercel.com`. You do not need to know their ATS.
   Already have board links? Paste those instead — `https://jobs.lever.co/spotify`,
   `https://boards.greenhouse.io/anthropic` and the other twelve platforms are recognised and
   fetched directly, with no lookup step.
3. Leave **Return only new and changed jobs** on if you plan to schedule it. Then set a daily
   schedule and each run returns only the delta. In a verified test run, a second pass over 205
   existing postings with this option on returned zero records — every one was correctly skipped
   as unchanged.
4. Get results from the dataset, the API, or push them straight into Google Sheets, Slack,
   Zapier, Make or n8n.

### Input configuration

The only field most people touch is **Company domains**. Everything else has a working default.

| Field | What it does | Default |
|---|---|---|
| Company domains | Websites to track, or board links if you have them | `ramp.com`, `vercel.com` |
| Return only new and changed jobs | Skips postings identical to the previous run | On |
| Include job descriptions | Full description text and HTML | On |
| Max jobs per company | Cost cap. `0` means unlimited. | `0` |
| Direct ATS sources | Skip auto-detection when you already know the slug | Empty |
| Proxy configuration | Not needed for these sources | Off |

### Pricing

#### How much does it cost to scrape company job listings?

You pay per job returned, plus a small fee per company whose ATS we resolve. Nothing else.

| Event | Price |
|---|---|
| Job scraped | $0.006 |
| Company ATS detected | $0.002 |

Worked example: tracking **50 companies** averaging **40 open roles** each costs
50 × $0.002 + 2,000 × $0.006 = **$12.10** for the first full run.

With **Return only new and changed jobs** switched on, a daily follow-up run typically returns
only a small fraction of that volume, since unchanged postings are never charged for the job-scraped
event. This is not a hypothetical: a verified run of 205 postings against this Actor, repeated
with the same input, returned zero new or changed records on the second pass.

### Output & integrations

Every record is returned in a stable schema with these fields: `sourceId`, `companySlug`,
`companyName`, `jobId`, `title`, `descriptionHtml`, `descriptionText`, `locationRaw`,
`locations`, `workplaceType`, `department`, `team`, `employmentType`, `seniority`,
`compensation`, `applyUrl`, `jobUrl`, `publishedAt`, `updatedAt`, `requisitionId`, `scrapedAt`,
`contentHash`.

Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API. Works with the
standard Apify integrations: Google Sheets, Slack, Zapier, Make, n8n, and webhooks.

`contentHash` is a fingerprint of the posting's content — use it to detect edits without
diffing every field yourself.

### Frequently asked questions

**Is this Actor affiliated with any of the supported platforms?**
No. This is an unofficial tool and is not affiliated with, endorsed by, or connected to any of
those companies. All product names are trademarks of their respective owners.

**Is this legal?**
It reads the same public job board endpoints that companies publish so their openings can be
distributed and indexed. It does not access private data, and it does not require or use any
login. You are responsible for how you use the data, including compliance with applicable data
protection law.

**Why not just use each ATS's API separately?**
You would need fourteen integrations with fourteen sets of field names and fourteen slug
formats - one returns XML, one needs a POST body and pagination, one only exposes its jobs
inside a JSON block embedded in a web page - and still no way to go from a company domain to
the right board. This does all of it in one call and gives you one schema.

**Which ATS platforms are supported?**
Fourteen: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio, BambooHR,
Teamtailor, Pinpoint, Rippling, Workday, Comeet and JOIN.

Five of them do not publish job descriptions on their public listing endpoint — BambooHR,
Rippling, Workday, JOIN and SmartRecruiters return title, location, department and the apply
link, but the description only exists on the posting page itself. Those records come back with
`descriptionText: null` rather than with a fabricated summary.

Two platforms cannot be auto-resolved from a domain, because their address is not derivable
from a company name:

- **Workday** needs a tenant, a data centre number and a free-text site name
  (`nvidia.wd5.myworkdayjobs.com/.../NVIDIAExternalCareerSite`).
- **Comeet** needs an account id and a separate token.

For both, a link on the company's careers page is picked up automatically. Otherwise pass them
through Direct ATS sources as `tenant:wdN:site` and `uid:token`.

**How many of my companies will be resolved?**
Two different things decide this, and it helps to keep them apart.

**1. Does the company use a supported ATS?** Fourteen platforms are supported (listed below).
Companies on anything else - Gem, iCIMS, Comeet, or an in-house system - cannot be returned at
all. On samples of tech companies picked before their ATS was known, this accounted for nearly
every miss.

**2. If they do, will we find it?** This is the part we control, and it is high: **43 of the 45
companies that had a supported board were resolved - about 19 out of 20.**

So the share of *your* list that comes back depends mostly on which platforms your companies
use. On a list of AI and infrastructure companies it was around 7 in 10; on a list of developer
tool companies, closer to 1 in 2.

Resolution works in two steps. First it reads the HTML your company's careers page serves and
looks for a link to a supported board. Many modern marketing sites render their listings in the
browser, so that HTML contains nothing to find. When that happens, the Actor derives likely
board names from the domain and queries each ATS directly, which skips the page entirely.

A derived board name can belong to a **different company** that happens to share a name, so
every derived match has to prove it belongs to you: the board must reference your domain or link
back to your website. If neither holds, the company is reported as unresolved along with the
board name we saw, and **its jobs are not returned** - handing you another company's jobs would
be worse than handing you none. If you know the match is right, pass it through Direct ATS
sources.

You are never charged for a company that is not resolved.

**A company I added returned nothing. Why?**
Detection reads the HTML a company's careers page serves and looks for a link to one of the five
supported boards. Two cases return nothing:

- The company uses an ATS not listed above.
- The careers page loads its listings in the browser with JavaScript, so the HTML the server
  sends contains no board link at all. This is common on marketing sites built as single-page
  apps, and no amount of retrying will surface a link that is not in the served HTML.

In both cases, use **Direct ATS sources** to supply the ATS and slug yourself — the run skips
detection for that company and fetches the board directly. Detection is a convenience, not a
requirement.

Companies that cannot be detected still cost a little run time (detection is capped at roughly
15 seconds per domain) but are never charged for, since `company-detected` only fires on a
successful match.

**Do I need a proxy?**
No. These sources are public JSON endpoints and work without one.

# Actor input Schema

## `companyDomains` (type: `array`):

Websites of the companies you want to track, e.g. vercel.com. We work out which ATS they use - you do not need to know it. Already have board links? Paste those instead (https://jobs.lever.co/spotify and the like) and they are fetched directly.

## `incremental` (type: `boolean`):

When on, postings identical to the previous run are skipped and not charged. Leave it on if you plan to run this on a schedule.

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

Turn off for faster runs that return only job metadata.

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

Cost cap. 0 means unlimited.

## `directSources` (type: `array`):

Skip auto-detection when you already know a company's ATS and slug. Fourteen platforms are supported. Two need a composite slug: Workday as tenant:wdN:site (e.g. nvidia:wd5:NVIDIAExternalCareerSite) and Comeet as uid:token (both values appear in the careers page embed).

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

Not needed for these sources. Change it only if a source starts blocking requests.

## Actor input object example

```json
{
  "companyDomains": [
    "ramp.com",
    "vercel.com"
  ],
  "incremental": true,
  "includeDescription": true,
  "maxJobsPerCompany": 0,
  "directSources": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One item per job: title, location, department, description, apply URL and publish date, normalised across every supported ATS.

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

Counts for the whole run: what was delivered, what was skipped, what failed, and any warnings. Read this to see whether the run did what you expected.

# 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 = {
    "companyDomains": [
        "ramp.com",
        "vercel.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("garnet_puppet/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 = {
    "companyDomains": [
        "ramp.com",
        "vercel.com",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("garnet_puppet/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 '{
  "companyDomains": [
    "ramp.com",
    "vercel.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call garnet_puppet/career-site-job-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,garnet_puppet/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/ocvbsQKMQdyuR2xCQ/builds/Qn7Z4G58pSpNegnJe/openapi.json
