# ZipRecruiter Jobs Scraper — US & Canada, Salary & Benefits (`axery/ziprecruiter-jobs-scraper`) Actor

Scrape ZipRecruiter listings with numeric salary on every row, benefits, structured location, and an honest disclosed-vs-predicted salary flag.

- **URL**: https://apify.com/axery/ziprecruiter-jobs-scraper.md
- **Developed by:** [Axery](https://apify.com/axery) (community)
- **Categories:** Jobs, Automation, News
- **Stats:** 2 total users, 1 monthly users, 28.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 results

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

## ZipRecruiter Jobs Scraper (US & Canada)

Scrapes job listings from **ziprecruiter.com** over plain HTTP — no browser, no login, no cookies.

### What makes this different

**Salary on every row — and an honest label on it.** In a live test run, **10 of 10 listings carried a numeric salary range**, against roughly 1 in 6 on SEEK. ZipRecruiter publishes pay as numbers, so nothing has to be parsed out of a display string.

The catch that other scrapers ignore: ZipRecruiter does not only republish salaries employers stated — it also *predicts* them, and does not make the difference obvious. In the same test run, **4 of the 10 figures were predictions**. This Actor surfaces that distinction:

```json
"salary": {
  "min": 120700, "max": 159100, "currency": "USD", "period": "year",
  "is_estimated": true, "salary_source": "SOURCE_PREDICTED",
  "raw": "USD 120,700 - USD 159,100 per year"
}
```

**Filter on `is_estimated` before building any pay benchmark.** A dataset that mixes modelled bands with disclosed ones will read as authoritative and be wrong. The `disclosedSalaryOnly` dataset view exists for exactly this.

**Benefits.** Medical, dental, vision, PTO, retirement and more, parsed per listing — data most job boards do not expose at all.

**Structured location, no parsing.** City, state, country code and a remote flag come straight from the source.

**Incremental mode.** Only listings not seen before, and you are charged only for those rows.

**One schema across boards.** Same record shape as the SEEK, JobStreet and Himalayas Actors, with a source-prefixed `job_id`.

### How it gets through

Two gates, documented because they are the whole difficulty of this target:

1. **TLS fingerprint.** Every Chrome and Safari impersonation profile returns HTTP 403 from a clean IP; `firefox133` returns 200 with the full document. The same headers under a Chrome fingerprint still fail, so this is a JA3-level check, not a header check. The client pins the Firefox profile deliberately.
2. **IP reputation.** ZipRecruiter refuses datacenter ranges regardless of fingerprint. **Apify Residential proxy is required, not optional.** The input schema defaults to it.

There is no public JSON search endpoint — `/api/jobs` returns 403 — so the client decodes the Next.js React Server Component payload embedded in the results page, where the full records live under `jobKeysMap`.

### Input

| Field | Type | Notes |
|---|---|---|
| `queries` | array | Job titles or keywords. |
| `location` | string | `Austin, TX`, `United States`, `Canada`. |
| `radius` | integer | Miles around the location. |
| `days` | enum | Posted within N days. |
| `refineBySalary` | integer | Minimum annual figure — filters on ZipRecruiter's number, which may be its own prediction. |
| `employmentType` | enum | Full time / Part time / Contract / Internship. |
| `maxItems` | integer | Per query. `0` = unlimited, bounded by the ceiling below. |
| `incremental` | boolean | Only listings not seen in previous runs. |
| `proxyConfiguration` | object | **Residential required.** |

### Known limits

- **Pagination ceiling around page 20.** Past it ZipRecruiter repeats listings rather than returning new ones. The Actor detects an all-duplicate page and stops cleanly instead of paying for the same rows twice. Narrow by location, radius or salary floor to reach deeper.
- **Roughly 40% of salaries are predicted.** Not a bug, and not hidden — see `is_estimated`.
- **No job category taxonomy.** ZipRecruiter does not expose one in this payload, so `category` is null. `benefits` and structured `location` more than make up for it.
- **Layout coupling.** Because the data comes from an RSC payload rather than a documented API, a front-end rewrite can break extraction. The client fails loudly with a specific message (`no jobKeysMap`) rather than silently returning zero rows.

### Sister Actors

| Actor | Covers |
|---|---|
| SEEK Jobs Scraper | Australia, New Zealand |
| JobStreet & JobsDB Jobs Scraper | Indonesia, Malaysia, Singapore, Philippines, Thailand, Hong Kong |
| Himalayas Remote Jobs Scraper | 100,000+ worldwide remote roles |
| Remote OK Jobs Scraper | Remote OK's public feed — read its README first |

### Local development

```bash
pip install -r requirements.txt
python test_local.py "python developer" --max 20 --out sample_output.json
python test_local.py "data engineer" --location "Austin, TX" --radius 25 --max 10
```

`sample_output.json` in this folder is real output from a live run, kept so the schema can be reviewed without running anything.

# Actor input Schema

## `queries` (type: `array`):

One or more job titles or keywords. Each runs independently and streams into the same dataset.

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

City, state or country, e.g. `Austin, TX`, `New York, NY`, `United States`, `Canada`. Combine with Radius to widen or tighten the catchment.

## `radius` (type: `integer`):

Search radius around the location, in miles. Leave blank for ZipRecruiter's default.

## `days` (type: `integer`):

Restrict to recently posted listings. Pair `Today` with incremental mode for a cheap daily monitor. Common ZipRecruiter values: 0 = any time, 1 = today, 5/10/30 = last N days.

## `refineBySalary` (type: `integer`):

Only return listings at or above this annual figure. Note this filters on ZipRecruiter's number, which may be its own prediction — check `salary.is_estimated` on the results.

## `employmentType` (type: `string`):

Restrict to a single employment type.

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

Maximum listings per keyword. Set to `0` for unlimited, which still stops at ZipRecruiter's pagination ceiling of about 20 pages — narrow by location, radius or salary floor to reach more.

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

Remember job ids between runs and return only listings not seen before. You are charged only for the new rows, so a daily monitor costs a fraction of a full re-scrape after the first run. Ids are stored in this Actor's key-value store under `seen_job_ids`.

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

Apify Proxy settings. ZipRecruiter refuses datacenter IP ranges — Residential is required, not optional. The Actor already uses the Firefox TLS fingerprint that ZipRecruiter accepts; the remaining gate is IP reputation.

## Actor input object example

```json
{
  "queries": [
    "python developer"
  ],
  "location": "Austin, TX",
  "radius": 25,
  "days": 0,
  "refineBySalary": 120000,
  "employmentType": "",
  "maxItems": 100,
  "incremental": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One row per listing: title, company, structured location, numeric salary with a disclosed-vs-predicted flag, benefits, employment type, posting date and both apply URLs.

## `coverage` (type: `string`):

What the run asked for versus what it returned, the pagination ceiling that applied, and the share of salaries that were predicted rather than disclosed.

# 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 = {
    "queries": [
        "python developer"
    ],
    "location": "United States",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("axery/ziprecruiter-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 = {
    "queries": ["python developer"],
    "location": "United States",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("axery/ziprecruiter-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 '{
  "queries": [
    "python developer"
  ],
  "location": "United States",
  "maxItems": 100
}' |
apify call axery/ziprecruiter-jobs-scraper --silent --output-dataset

```

## MCP server setup

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