# ZipRecruiter Jobs Scraper — URL or Keywords, Emails (`memo23/apify-ziprecruiter-scraper`) Actor

Scrape public ZipRecruiter job listings in the US, Canada, UK, Ireland, India, Germany, France and Australia: title, company, salary, benefits, workplace type and apply links. Many keywords and cities per run, only-new-jobs mode, Slack, Discord and Telegram alerts. JSON/CSV.

- **URL**: https://apify.com/memo23/apify-ziprecruiter-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, Lead generation
- **Features:** Limited permissions
- **Stats:** 480 total users, 70 monthly users, 99.9% runs succeeded, 12 bookmarks
- **User rating**: 2.72 out of 5 stars

## Pricing

from $2.50 / 1,000 job results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ZipRecruiter Scraper — Jobs by Keyword or URL + Employer Emails

> **Disclaimer:** This is an independent, unofficial tool. It is not affiliated with, endorsed by, or sponsored by ZipRecruiter, Inc. or any of its subsidiaries. "ZipRecruiter" and all related names, marks, and logos are trademarks of their respective owners and are used here for descriptive (nominative) purposes only. This Actor accesses only publicly available data.

Scrape **ZipRecruiter job listings** at scale: search several keywords in several locations in one run, or paste any ZipRecruiter search URL, in the United States, Canada, the UK, Ireland, India, Germany, France and Australia. You get full postings with salary, benefits, employment and workplace type, company details and direct/external apply links. Scheduled runs can return only jobs that are new since the last run and post them to Slack, Discord, Telegram or your webhook. Flip one toggle and the scraper also finds a **verified employer contact email** for each job's company — turning job data into ready-to-use recruiting and lead-gen lists.

### Pricing (pay per event)

| Event | Price | When charged |
|---|---|---|
| Actor start | $0.005 | Once per run (per GB of run memory) |
| **Job result** | **$0.0025** | Per job pushed to the dataset |
| Employer email found | $0.052 | Only when opt-in enrichment finds a verified contact email |

**Example:** 1,000 jobs ≈ **$2.50**. With email enrichment on and a ~30% hit rate: 1,000 jobs + 300 emails ≈ **$18.10**. Emails are billed only when found — misses are free.

### Why Scrape ZipRecruiter?

ZipRecruiter is one of the largest job posting platforms in the United States, aggregating millions of job listings from various sources. Scraping ZipRecruiter offers invaluable data for:

1. Labor Market Analysis: Track job trends, salary ranges, and in-demand skills across industries and locations.
2. Competitive Intelligence: Monitor hiring patterns of competitors and identify emerging industry trends.
3. Recruitment Strategies: Gain insights into job descriptions, required qualifications, and benefits to optimize your own job postings.
4. Career Research: Analyze job requirements and salaries for specific roles to guide career decisions or negotiations.
5. Economic Research: Use job posting data as a leading indicator of economic health and industry growth.
6. AI and Machine Learning: Build robust datasets for training job matching algorithms or predictive models for career progression.

### What Can You Scrape?

Every job becomes one flat row, ready for JSON, CSV or Excel:

- **Job:** title, full description (text, HTML or Markdown), employment type, workplace type (remote, hybrid or on-site), posting date and expiry date
- **Pay:** salary range, currency and period (hourly, yearly…), the same range as a yearly figure, a readable salary string, and where the pay came from (stated by the employer, read from the job text, estimated, or not shown on the site)
- **Requirements:** seniority level, months of experience, required and preferred education, and on job pages the skills, industry and occupation category
- **Benefits** listed on the job, such as medical, dental or paid time off
- **Employer:** company name, company page and logo; industry, size, founding year and headquarters when the job page carries a company profile
- **Location:** city, state or region, postal code when listed, country code, and a remote flag
- **Apply:** an apply link on every row, and the employer's own application (ATS) link for jobs that apply off-site
- **Tracking:** job id, the search the job came from, when it was scraped and, in incremental mode, whether it is new, updated or a repost
- **Employer contact email** (opt-in, see below)

### How it works

Paste ZipRecruiter search or job URLs, or fill in keywords and locations. The actor reads the search results page by page up to your limits, then opens each job's page for the full description (or skips that step in listing-only mode).

### Features

- **Many searches in one run.** Give several keywords and several locations: every keyword is searched in every location (up to 100 searches). A job listed by more than one search is saved once, so you are not charged twice for it. `maxItemsPerSearch` gives every search its share of `maxItems`.
- **Past the 60-job limit.** In the US and Canada, ZipRecruiter shows at most 3 pages of a search, about 60 jobs, however many it counts. When a search counts more and `maxItems` asks for more, the run also searches narrower versions of it (by experience level, job type, workplace, pay band and posting date) and saves each job they add once, until `maxItems` is reached. On by default; turn off `splitLargeSearches` to keep each search to what ZipRecruiter shows. The run log says how many narrower searches ran and what they added.
- **Four countries.** United States and Canada (ziprecruiter.com), United Kingdom (ziprecruiter.co.uk) and Ireland (ziprecruiter.ie).
- **Listing-only mode.** Turn off `scrapeJobDetails` and rows come straight from the search results: title, company, location, pay and its source, workplace type, employment type, benefits, seniority, experience, education, posting date, a description snippet and the apply link. Many times faster, since no job page is opened.
- **Filters:** keyword, location, radius, posted within N days, company, employment type, salary range, workplace (remote / hybrid / on-site), experience level (no experience / junior / mid / senior), 1-Click Apply only, external apply only.
- **Only new jobs (incremental mode)** for scheduled runs: jobs unchanged since earlier runs are skipped before their page is fetched and are not charged. Rows are tagged `new`, `updated` or `repost`.
- **Notifications** when the run ends: any webhook (n8n, Make, Zapier, your backend), Slack, Discord or Telegram.
- **Output for AI agents and pipelines:** description as text, HTML and/or Markdown, a length cap, compact rows, empty fields left out.
- **Employer contact emails** (opt-in), see below.

#### Countries

| Country | Site | What you get |
|---|---|---|
| United States | ziprecruiter.com | Everything, every filter |
| Canada | ziprecruiter.com | Everything, every filter. Use a Canadian location (`Toronto, ON`) or leave it empty for all of Canada. |
| United Kingdom | ziprecruiter.co.uk | Search cards: title, company, location, date, snippet, apply link. UK jobs open on the source site, so there is no full description. |
| Ireland | ziprecruiter.ie | Full job pages: full description, dates, employer apply link |
| India | ziprecruiter.in | Full job pages, like Ireland |
| Germany | ziprecruiter.de | Full job pages; the few jobs that open on the source site come as their search card |
| France | ziprecruiter.fr | Full job pages for about half the jobs; the rest open on the source site and come as their search card: title, company, location, date and apply link, with no description (French cards show none) |
| Australia | ziprecruiter.com.au | Full job pages for most jobs; about a quarter open on the source site and come as their search card |

Set `searchCountry` to `US`, `CA`, `UK`, `IE`, `IN`, `DE`, `FR` or `AU`, or paste a search URL from any of these sites. The sites outside North America have fewer filters than ziprecruiter.com: keyword, location, company, job type, on-site (not France), and **Newest first** (`searchSortBy: "date"`). Their job type filter is the site's own: Germany has full time, part time, contract and temporary; India and Australia only full time; France only CDI (permanent contract). There is no radius, salary, experience or apply-type filter there, and "posted within N days" is applied by the actor to the cards' dates. ziprecruiter.com has no sort order of its own: use `searchPostedWithinDays` for recent jobs.

#### Only new jobs (incremental mode)

Set `"incrementalMode": true` on a scheduled task. The first run saves every job. Every later run compares each search result with what earlier runs saw and saves only jobs that are new, changed (title, location, pay or employment type) or reposted under a new id. ZipRecruiter often has several listings for one job (an opening posted for each campus, or the same post with slightly different titles or pay) and shows one of them at a time. So a job that switches back to a version it showed before is not counted as changed, and a version not seen before counts as changed once the next run still shows it. A real change is saved one run later, and switching between listings is never charged. Rows get `changeType` and `firstSeenAt`. Unchanged jobs are skipped before their page is fetched and are never charged. The state lives in the key-value store `ziprecruiter-incremental` in your account (with the actor's id added to the name if another listing of this scraper created that store first), one record per search; set `incrementalStateKey` to name it yourself. `skipReposts` drops reposted jobs.

#### Notifications

When the run ends, the actor can send its job count, a dataset link and the first jobs (`notifyMaxJobs`, default 10) to:

- a webhook (`notifyWebhookSecretUrl`, optional `notifyWebhookSecretHeaders`): a JSON POST with the jobs as rows,
- Slack (`notifySlackSecretUrl`, an incoming webhook),
- Discord (`notifyDiscordSecretUrl`, a channel webhook),
- Telegram (`notifyTelegramBotToken` + `notifyTelegramChatId`).

The URLs, the webhook headers and the token are stored encrypted and are never written to the run log. `notifyOnlyWhenJobsFound` skips the message when the run saved nothing (with incremental mode: nothing new).

### How to Use

1. Set Up: Ensure you have an Apify account and access to the Apify platform.
2. Configure Input: paste search or job URLs, or fill in keywords, locations and the country.
3. Proxy: nothing to set on a paid Apify plan, the actor uses its own US residential proxy. On the free plan, use Apify RESIDENTIAL proxies with country US.
4. (Optional) Set Max Items (parameter: maxItems) to limit the number of job listings to scrape.
5. (Optional) Enable `externalApplyOnly` to keep only jobs that redirect candidates to an external apply flow.
6. Run the actor and obtain the extracted data in your preferred format.
7. Data Collection: The scraper will output data in your chosen format, which can include JSON, CSV, Excel, or others as supported by Apify.

### Input Configuration

Several keywords in several cities, the last 7 days, listing-only, only new jobs on each scheduled run, posted to Slack:

```json
{
    "searchKeywords": ["data analyst", "data scientist"],
    "searchLocations": ["New York, NY", "Austin, TX"],
    "searchPostedWithinDays": 7,
    "searchWorkplace": "hybrid",
    "searchExperienceLevels": ["mid", "senior"],
    "scrapeJobDetails": false,
    "maxItems": 200,
    "maxItemsPerSearch": 50,
    "incrementalMode": true,
    "notifySlackSecretUrl": "https://hooks.slack.com/services/…"
}
```

Newest software engineer jobs in London:

```json
{
    "searchCountry": "UK",
    "searchJobKeyword": "software engineer",
    "searchLocation": "London",
    "searchSortBy": "date",
    "maxItems": 100
}
```

Pasted URLs work as before. Here's an example of how to set up the input for scraping fire alarm technician jobs in New York:

```json
{
    "startUrls": [
        {
            "url": "https://www.ziprecruiter.ie/jobs/search?q=backend&l=&lat=&long="
        },
        {
            "url": "https://www.ziprecruiter.com/jobs-search?form=jobs-landing&search=fire+alarm+technician&location=New+York%2C+NY"
        }
    ],
    "maxItems": 1000,
    "maxConcurrency": 10,
    "minConcurrency": 1,
    "maxRequestRetries": 10,
    "externalApplyOnly": false,
    "enrichEmails": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
}
```

### Output Structure

Each row is flat (CSV-friendly). `applyUrl` is always set — the employer apply link when ZipRecruiter exposes one, otherwise the job page. `inputUrl` is the search URL that produced the row.

When `externalApplyOnly` is enabled, only items with `isExternalApply: true` are emitted.

```json
{
    "title": "Senior AI Engineer, Banking Technology",
    "company": "Citi",
    "companyUrl": "https://www.citigroup.com/",
    "companyLogo": "https://www.ziprecruiter.com/svc/fotomat/public-nosensitive-ziprecruiter-logos/company/citi.png",
    "city": "Jersey City",
    "state": "NJ",
    "country": "US",
    "postalCode": null,
    "location": "Jersey City, NJ",
    "isRemote": false,
    "workplaceType": "hybrid",
    "employmentType": "FULL_TIME",
    "salaryMin": 142320,
    "salaryMax": 213480,
    "salaryCurrency": "USD",
    "salaryPeriod": "YEAR",
    "salary": "$142,320 - $213,480 / YEAR",
    "salarySource": "employer",
    "salaryMinAnnual": 142320,
    "salaryMaxAnnual": 213480,
    "benefits": ["Medical", "Dental", "Paid time off", "Retirement"],
    "seniority": "senior",
    "experienceMonths": 60,
    "educationRequired": "Bachelor's degree",
    "educationPreferred": "Master's degree",
    "skills": ["Python", "Machine Learning", "Cloud"],
    "industry": "Finance",
    "occupationalCategory": "15-1252.00: Software Developers",
    "description": "Role Overview: Citi's Banking Technology organization is seeking...",
    "descriptionHtml": "<p>Role Overview: ...</p>",
    "datePosted": "2026-09-12T05:23:32Z",
    "lastPostedAt": "2026-10-30T14:02:11Z",
    "validThrough": "2026-10-12",
    "id": "26984690",
    "url": "https://www.ziprecruiter.com/c/Citi/Job/Senior-AI-Engineer/-in-Jersey-City,NJ?jid=3bf1f2f8b6318d6c",
    "applyUrl": "https://www.ziprecruiter.com/c/Citi/Job/Senior-AI-Engineer/-in-Jersey-City,NJ?jid=3bf1f2f8b6318d6c",
    "isExternalApply": false,
    "externalApplyUrl": "",
    "inputUrl": "https://www.ziprecruiter.com/jobs-search?search=software+engineer&location=New+York%2C+NY",
    "directApply": false,
    "scrapedAt": "2026-09-14T00:00:00.000Z",
    "source": "schema.org/JobPosting",
    "descriptionIncomplete": false,
    "detailsFetched": true
}
```

#### Field descriptions

- `title` / `company` / `companyUrl` / `companyLogo` — job and employer
- `city` / `state` / `country` / `postalCode` / `location` — parsed place; `location` is `"City, ST"`, `country` a code (`US`, `CA`, `GB`, `IE`)
- `isRemote` — true when the listing is remote
- `workplaceType` — `remote`, `hybrid`, `on-site` or `remote-optional`
- `employmentType` — e.g. `FULL_TIME`, `CONTRACTOR`, `PART_TIME`
- `salaryMin` / `salaryMax` / `salaryCurrency` / `salaryPeriod` / `salary` — numeric range plus a display string
- `salarySource` — where the pay came from: `employer` (stated by the employer), `parsed` (read from the job text), `estimated` (ZipRecruiter's estimate, labelled as one on the site) or `not_shown` (ZipRecruiter holds these figures but the site does not show them). Filter on it if you only want stated pay.
- `salaryMinAnnual` / `salaryMaxAnnual` — the range as a yearly figure, for comparing hourly and yearly jobs
- `seniority` — `no_experience`, `junior`, `mid` or `senior`, the same words `searchExperienceLevels` takes
- `experienceMonths` — months of experience asked for
- `educationRequired` / `educationPreferred` — e.g. `Bachelor's degree`
- `skills` / `industry` / `occupationalCategory` — from the job page, so empty in listing-only mode
- `benefits` — e.g. `["Medical", "Dental", "Paid time off"]`
- `description` — full plain text (Open Graph teasers are skipped; `descriptionIncomplete` is true if only a teaser was available). In listing-only mode and for UK jobs, the search result's snippet.
- `descriptionHtml` — original HTML when present
- `descriptionMarkdown` — only when `descriptionFormats` includes `markdown`
- `datePosted` — the posting date ZipRecruiter gives for the job, usually when it was first posted. An employer can keep a job open for years, so this can be long ago
- `lastPostedAt` — when the job was last reposted: the date ZipRecruiter shows as "Posted 3 days ago" (US and Canada; the other sites show a single date, in `datePosted`)
- `validThrough`
- `id` — ZipRecruiter job id (`jid`; `uk-…`, `ie-…`, `in-…`, `de-…`, `fr-…`, `au-…` on the sites outside North America)
- `url` — job page
- `applyUrl` — apply link (never empty)
- `isExternalApply` / `externalApplyUrl` — employer ATS URL when ZipRecruiter exposes one
- `inputUrl` — the search URL for this run
- `source` — where the row was read: `schema.org/JobPosting` / `page-dom` (job page), `search card` (listing-only), `ziprecruiter.co.uk` (UK card)
- `detailsFetched` — false when the row comes from the search results alone
- `companyIndustry` / `companySize` / `companyFoundedYear` / `companyHeadquarters` / `companyWebsite` — only when the job page carries ZipRecruiter's company profile
- `changeType` / `firstSeenAt` — only in incremental mode
- `contactEmail` / `contactWebsite` — only when `enrichEmails` is on and a contact is found

The older nested card dump (`Title`, `OrgName`, `jobDetails`, …) is no longer written.

### External Apply Notes

- `externalApplyOnly` works best on search result URLs because the SERP payload contains the strongest apply metadata.
- Some individual job pages still expose `directApply` in their structured data even when the originating SERP card was external. The actor preserves the stronger SERP signal so external jobs are not dropped.
- `externalApplyUrl` may still point to a ZipRecruiter redirect URL. The final third-party destination is usually embedded behind that redirect token.

### Employer Contact Emails (opt-in)

Set `"enrichEmails": true` and every job record gains three extra fields:

- `contactEmail` — best verified email for the hiring company
- `contactWebsite` — the company website the email was found on
- `emailEnrichment` — full enrichment detail (all emails found, source, confidence)

Discovery uses the company name + location with a business-domain guard (no residential or junk domains) and on-site contact-page scraping. Charged only on success — $0.052 per email actually found.

### FAQ

**Can I reuse an input written for another ZipRecruiter scraper?**
Yes. The short field names other actors use are accepted as aliases and mapped automatically: `search` → keyword, `location`, `radiusMiles`, `daysPosted`, `remoteOnly`, `jobType` (`any` = all types), `proxyConfiguration`, and `query` (one search, or several separated by commas) with `maxResults` or `limit` for the row cap. `startUrls` takes plain URL strings as well as `{ "url": … }` objects. Paste their input and it runs here unchanged; anything you set explicitly wins over an alias.

**How much does a run cost?** $0.0025 per job + $0.005 actor start. 1,000 jobs ≈ $2.50. Email enrichment adds $0.052 per email found (only when found). In incremental mode, jobs skipped as unchanged cost nothing.

**Do I need proxies?** Not on a paid Apify plan: the actor uses its own US residential proxy. On the free plan it uses the proxy you set (Apify RESIDENTIAL, US works).

**Which sites are supported?** ziprecruiter.com (United States and Canada), ziprecruiter.co.uk and ziprecruiter.ie. See [Countries](#countries) for what each one returns.

**Why does ZipRecruiter show only about 60 jobs of a search that counts 1,000?** It shows at most 3 pages of any search, and deeper pages come back empty. With `splitLargeSearches` on (the default), the run also searches narrower versions of a big search (one per experience level, then job type, workplace, pay band and posting date) and keeps going until `maxItems` is reached, so a 720-job search can return several hundred jobs. ZipRecruiter's count is an estimate, and a very large search (tens of thousands of jobs nationwide) can still hold more than the narrower searches show: add keywords in `searchKeywords` or cities in `searchLocations` to go further.

**Can I watch a search and get only new jobs?** Yes. Schedule the actor with `incrementalMode: true` and a notification channel: each run saves and sends only jobs that are new or changed since the previous run.

**Can I search many keywords and cities at once?** Yes. Put them in `searchKeywords` and `searchLocations`; every keyword is searched in every location (up to 100 searches), and a job found by two searches is saved and charged once.

**Can I keep only external-apply jobs?** Yes — set `externalApplyOnly: true` to keep only jobs that redirect to an external ATS (ideal for sourcing direct employer application links).

### Support

- For issues or feature requests, open the **Issues** tab on this actor's page.
- If you need customization or have questions, feel free to contact the author:
  - Author's website: <https://muhamed-didovic.github.io/>
  - Email: <muhamed.didovic@gmail.com>
  - My Apify Actors/Scrapers: <https://apify.com/memo23>

### Additional Services

- Request customization or whole dataset: <muhamed.didovic@gmail.com>
- If you need anything else scraped, or this actor customized, email: <muhamed.didovic@gmail.com>
- For API services of this scraper (no Apify fee, just usage fee for the API), contact: <muhamed.didovic@gmail.com>

### Explore More Scrapers

Job-market data from the same author: [Indeed](https://apify.com/memo23/apify-indeed-cheerio-ppr), [Glassdoor](https://apify.com/memo23/glassdoor-scraper-ppr), [LinkedIn Jobs](https://apify.com/memo23/linkedin-jobs-scraper), [Wellfound](https://apify.com/memo23/wellfound-jobs-scraper), [Naukri](https://apify.com/memo23/naukri-scraper), [StepStone](https://apify.com/memo23/stepstone-search-cheerio-ppr), [Monster](https://apify.com/memo23/monster-scraper), [Seek](https://apify.com/memo23/seek-scraper), [Reed](https://apify.com/memo23/reed-jobs-scraper), [TotalJobs](https://apify.com/memo23/totaljobs-scraper) — full catalog at [apify.com/memo23](https://apify.com/memo23).

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API. The same scraper is published as `memo23/ziprecruiter-scraper` and `memo23/apify-ziprecruiter-scraper`; both take the input below and return the same rows at the same price.

**Purpose:** Scrapes ZipRecruiter job listings (United States and Canada on `.com`, United Kingdom on `.co.uk`, Ireland on `.ie`) into one flat row per job, from search/job URLs or search fields, with optional employer contact-email enrichment.

**Minimal input:**

```json
{ "startUrls": [{ "url": "https://www.ziprecruiter.com/jobs-search?search=fire+alarm+technician&location=New+York%2C+NY" }], "maxItems": 25 }
```

When `startUrls` is empty, drive the search with fields instead: `searchJobKeyword` or `searchKeywords` (a list), `searchLocation` or `searchLocations` (a list), `searchCountry` (`US`, `CA`, `UK`, `IE`, `IN`, `DE`, `FR`, `AU`), and optionally `searchCompany`, `searchEmploymentType`, `searchWorkplace` (`remote`, `hybrid`, `on_site`), `searchExperienceLevels`, `searchEasyApply`, `searchMinSalary`/`searchMaxSalary`, `searchRadiusMiles`, `searchPostedWithinDays`, `searchSortBy` (sites outside the US and Canada).

**Output:** one flat row per job: `title`, `company`, `location`, `city`, `state`, `country`, `workplaceType`, `employmentType`, `salary`, `salarySource`, `salaryMin`, `salaryMax`, `salaryMinAnnual`, `salaryMaxAnnual`, `salaryCurrency`, `salaryPeriod`, `benefits`, `seniority`, `experienceMonths`, `educationRequired`, `educationPreferred`, `skills`, `industry`, `description`, `descriptionHtml`, `datePosted`, `lastPostedAt`, `id`, `url`, `applyUrl`, `isExternalApply`, `externalApplyUrl`, `detailsFetched`; plus `changeType`/`firstSeenAt` in incremental mode and `contactEmail`/`contactWebsite`/`emailEnrichment` when `enrichEmails` is on.

**Behaviors an agent should know:**

- `startUrls` win; the search fields are used only when `startUrls` is empty.
- `maxItems` caps job rows (default 100). Set it to bound cost. `maxItemsPerSearch` splits it across searches.
- One ZipRecruiter search shows about 60 jobs; `splitLargeSearches` (default on) runs narrower versions of a bigger search to reach `maxItems` in the US and Canada.
- `scrapeJobDetails: false` builds rows from search results alone: much faster, with a description snippet instead of the full text.
- `incrementalMode: true` returns only jobs that are new or changed since earlier runs of the same search; unchanged jobs are not charged.
- `compactOutput`, `descriptionFormats: ["markdown"]` and `descriptionMaxLength` keep rows small for an LLM's context.
- `externalApplyOnly` emits only jobs that open an external apply flow.
- `enrichEmails` (opt-in, best-effort) discovers the employer website and adds contact-email columns; the email event is billed only when a verified email is found. Misses are free.
- Billing: $0.005 actor start (per GB), $0.0025 per job result, $0.052 per employer email found.
- Pages that fail to load are retried automatically. `maxUnblockerRequests` (default 40) caps the extra retries one run may make.

### ⚠️ Disclaimer

This actor extracts only publicly available data from ZipRecruiter job listings, for legitimate uses such as market research, recruiting and analytics. It is not affiliated with or endorsed by ZipRecruiter. You are responsible for using the extracted data in compliance with applicable laws and ZipRecruiter's terms of service.

### SEO Keywords

ZipRecruiter scraper, ZipRecruiter job scraper, ZipRecruiter jobs API, scrape ZipRecruiter listings, job listings scraper, job board scraper, employer email finder, recruiting leads scraper, hiring data scraper, salary data scraper, remote jobs scraper, ZipRecruiter search URL scraper, US jobs dataset, Canada jobs scraper, UK jobs scraper, Ireland jobs scraper, new job alerts, job monitoring, recruitment lead generation, apply link extractor

# Actor input Schema

## `startUrls` (type: `array`):

One URL per row. Example: a full `jobs-search` URL with your keyword and location in the query string. A `jobs-search` URL without a location searches the whole United States.

## `searchJobKeyword` (type: `string`):

Job title or skill, e.g. `nurse`, `react developer`. Maps to ZipRecruiter `search`.

## `searchKeywords` (type: `array`):

Search several keywords in one run. Every keyword is searched in every location, and a job listed by more than one search is saved once.

## `search` (type: `string`):

Job keyword. Alias of Search job keyword — either works.

## `searchLocation` (type: `string`):

City, state, region, or country, e.g. `Austin, TX`. Leave empty to search the whole country. Maps to `location`.

## `searchLocations` (type: `array`):

Search several locations in one run, e.g. `Austin, TX`, `Denver, CO`. Every keyword is searched in every location (up to 100 searches per run).

## `searchCountry` (type: `string`):

Which ZipRecruiter to search. **United States** and **Canada** search ziprecruiter.com (use a Canadian city such as `Toronto, ON`, or leave the location empty for all of Canada). **United Kingdom**, **Ireland**, **India**, **Germany**, **France** and **Australia** search ziprecruiter.co.uk, .ie, .in, .de, .fr and .com.au. Irish, Indian and German jobs are read in full. UK jobs, about half of French and a quarter of Australian jobs open on the source site, so their rows carry the search card (no full description; French cards have no snippet either). These sites have no radius, salary, experience or apply-type filter; India and Australia filter job type by full time only, France by CDI only.

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

City, state or ZIP; leave empty to search the whole United States. Alias of Search location.

## `searchCompany` (type: `string`):

Narrow results to one employer. Maps to `company`. Leave empty for all companies.

## `searchPostedWithinDays` (type: `integer`):

Only show jobs posted in the last N days. Maps to ZipRecruiter `days`.

## `daysPosted` (type: `integer`):

Only jobs posted within N days. Alias of Posted within days.

## `searchRadiusMiles` (type: `integer`):

Search radius around the location. Maps to `radius`.

## `radiusMiles` (type: `integer`):

Search radius in miles. Alias of Search radius.

## `searchEmploymentType` (type: `string`):

Maps to `refine_by_employment` (employment_type:…).

## `jobType` (type: `string`):

Employment type; 'any' is treated as all. Alias of Employment type.

## `searchRemoteOnly` (type: `boolean`):

Maps to `refine_by_location_type=only_remote`. Same as Workplace = Remote.

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

Remote jobs only. Alias of Remote only.

## `searchWorkplace` (type: `string`):

Remote, hybrid or on-site jobs only. Overrides **Remote only** when set. The UK and Irish sites list every job as on-site, so only On-site applies there.

## `searchExperienceLevels` (type: `array`):

Only jobs for these experience levels. Leave empty for all. ziprecruiter.com (US and Canada) only.

## `searchEasyApply` (type: `boolean`):

Only jobs you apply to on ZipRecruiter (1-Click Apply), no redirect to the employer's site. Uses the site's own filter (`refine_by_apply_type=one_click_apply`) and each job's apply button.

## `searchMinSalary` (type: `integer`):

Maps to `refine_by_salary`. Leave empty for no floor.

## `searchMaxSalary` (type: `integer`):

Maps to `refine_by_salary_ceil`. Leave empty for no ceiling.

## `searchSortBy` (type: `string`):

Newest first is available on the UK, Irish, Indian, German, French and Australian sites. ziprecruiter.com (US and Canada) always returns best match: use **Posted within (days)** there to get recent jobs.

## `searchStartPage` (type: `integer`):

Maps to `page` (1 = first page of results).

## `scrapeJobDetails` (type: `boolean`):

On: every job's own page is read, for the full description, validity date and apply link. Off (listing-only): rows come straight from the search results, with title, company, location, pay, workplace type, employment type, benefits, posting date, a description snippet and the apply link. Much faster. Rows have `detailsFetched: false`.

## `deduplicateJobs` (type: `boolean`):

When several searches (keywords, locations, URLs) list the same job, save it once. Turn off to keep one row per search that listed it.

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

Maximum number of jobs to save in this run. In the US and Canada, ZipRecruiter shows at most about 60 jobs per search (3 pages). With "Search past the 60-job limit" on, the run also searches narrower versions of a bigger search to reach this number.

## `maxItemsPerSearch` (type: `integer`):

Cap for each search (each keyword × location, or each pasted search URL), so every search of a multi-search run gets its share. 0 = no cap per search; the run still stops at Max items.

## `splitLargeSearches` (type: `boolean`):

In the US and Canada, ZipRecruiter shows at most about 60 jobs per search, however many it counts. When a search counts more and Max items asks for more, the run also searches narrower versions of it (by experience level, job type, workplace, pay band and posting date) and saves each job they add once. Off: each search stops at the jobs ZipRecruiter shows.

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

Parallel page requests (higher = faster, more load).

## `minConcurrency` (type: `integer`):

Minimum parallel requests.

## `maxRequestRetries` (type: `integer`):

Retries per request before giving up.

## `externalApplyOnly` (type: `boolean`):

Keep only jobs that open an external apply flow. Strongest on search result pages.

## `enrichEmails` (type: `boolean`):

If enabled, tries to find a contact email for each employer by discovering the company's website (Clearbit) and reading its contact/about pages. Adds contactEmail + contactWebsite columns plus a detailed emailEnrichment object. Best-effort.

## `descriptionFormats` (type: `array`):

Which description fields each row carries: `description` (plain text), `descriptionHtml`, `descriptionMarkdown`. Default: text and HTML.

## `descriptionMaxLength` (type: `integer`):

Cut the text and Markdown descriptions to this many characters (on a word boundary). 0 = no limit. HTML is never cut.

## `compactOutput` (type: `boolean`):

Core fields only: id, title, company, location, workplace type, employment type, salary, date posted, description, URL, apply URL. For AI agents, MCP tools and light pipelines.

## `omitEmptyFields` (type: `boolean`):

Drop fields that are empty (null, empty text, empty list) from each row.

## `incrementalMode` (type: `boolean`):

Compare every job with earlier runs of the same search. Rows get `changeType` (`new`, `updated` or `repost`) and `firstSeenAt`. A change counts once the next run still shows it, since ZipRecruiter switches between listings of the same job. The first run saves everything. The state is kept in the key-value store `ziprecruiter-incremental` in your account.

## `incrementalStateKey` (type: `string`):

Name of the saved state. Leave empty and each search (its URLs and filters) gets its own state automatically. Set a name to share one state between tasks or keep it across input changes.

## `skipReposts` (type: `boolean`):

Skip a job whose title, company and city match a job already seen under another id (the same job posted again).

## `notifyWebhookSecretUrl` (type: `string`):

Receives a JSON POST when the run ends: counts, the dataset link and the first jobs (up to Max jobs per notification).

## `notifyWebhookSecretHeaders` (type: `object`):

Optional headers for the webhook POST, e.g. `{"Authorization": "Bearer …"}`. Stored encrypted.

## `notifySlackSecretUrl` (type: `string`):

A Slack incoming webhook (https://hooks.slack.com/services/…).

## `notifyDiscordSecretUrl` (type: `string`):

A Discord channel webhook (Server settings → Integrations → Webhooks).

## `notifyTelegramBotToken` (type: `string`):

From @BotFather. Needs the chat ID below.

## `notifyTelegramChatId` (type: `string`):

The chat, group or channel to post to, e.g. `-1001234567890` or `@yourchannel`. Add the bot to it first.

## `notifyMaxJobs` (type: `integer`):

How many jobs a message lists (1-50). The rest are in the dataset.

## `notifyOnlyWhenJobsFound` (type: `boolean`):

Skip the notification when the run saved no jobs (with incremental mode: nothing new).

## `proxy` (type: `object`):

Residential proxies recommended. The default works out of the box.

## `maxUnblockerRequests` (type: `integer`):

Pages that fail to load are retried automatically. This caps how many extra retry requests one run may make. Default 40.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.ziprecruiter.com/jobs-search?form=jobs-landing&search=fire+alarm+technician&location=New+York%2C+NY"
  ],
  "searchJobKeyword": "software engineer",
  "searchLocation": "New York, NY",
  "searchCountry": "US",
  "searchRadiusMiles": 25,
  "searchEmploymentType": "all",
  "searchRemoteOnly": false,
  "searchWorkplace": "any",
  "searchEasyApply": false,
  "searchSortBy": "relevance",
  "searchStartPage": 1,
  "scrapeJobDetails": true,
  "deduplicateJobs": true,
  "maxItems": 100,
  "maxItemsPerSearch": 0,
  "splitLargeSearches": true,
  "maxConcurrency": 10,
  "minConcurrency": 1,
  "maxRequestRetries": 30,
  "externalApplyOnly": false,
  "enrichEmails": false,
  "descriptionFormats": [
    "text",
    "html"
  ],
  "descriptionMaxLength": 0,
  "compactOutput": false,
  "omitEmptyFields": false,
  "incrementalMode": false,
  "skipReposts": false,
  "notifyMaxJobs": 10,
  "notifyOnlyWhenJobsFound": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "maxUnblockerRequests": 40
}
```

# Actor output Schema

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

No description

# 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 = {
    "startUrls": [
        "https://www.ziprecruiter.com/jobs-search?form=jobs-landing&search=fire+alarm+technician&location=New+York%2C+NY"
    ],
    "searchJobKeyword": "software engineer",
    "searchLocation": "New York, NY",
    "searchRadiusMiles": 25,
    "searchStartPage": 1,
    "maxItems": 100,
    "maxConcurrency": 10,
    "minConcurrency": 1,
    "maxRequestRetries": 30,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/apify-ziprecruiter-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 = {
    "startUrls": ["https://www.ziprecruiter.com/jobs-search?form=jobs-landing&search=fire+alarm+technician&location=New+York%2C+NY"],
    "searchJobKeyword": "software engineer",
    "searchLocation": "New York, NY",
    "searchRadiusMiles": 25,
    "searchStartPage": 1,
    "maxItems": 100,
    "maxConcurrency": 10,
    "minConcurrency": 1,
    "maxRequestRetries": 30,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/apify-ziprecruiter-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 '{
  "startUrls": [
    "https://www.ziprecruiter.com/jobs-search?form=jobs-landing&search=fire+alarm+technician&location=New+York%2C+NY"
  ],
  "searchJobKeyword": "software engineer",
  "searchLocation": "New York, NY",
  "searchRadiusMiles": 25,
  "searchStartPage": 1,
  "maxItems": 100,
  "maxConcurrency": 10,
  "minConcurrency": 1,
  "maxRequestRetries": 30,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call memo23/apify-ziprecruiter-scraper --silent --output-dataset

```

## MCP server setup

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