# Dice.com Scraper - Tech Jobs, Salaries, Skills & Employers (`abotapi/dice-com-scraper`) Actor

Scrape tech job listings from Dice.com by keyword, filters, or URL. Extract job titles, companies, recruiter types, salaries, locations, skills, posted dates, apply links or emails, and full job descriptions. Auto-paginates with pay-per-result pricing and no subscription required.

- **URL**: https://apify.com/abotapi/dice-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 3 total users, 1 monthly users, 93.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 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.
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?

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

## Dice.com Scraper: Tech Job Listings with Salaries, Skills & Apply Contacts

Dice.com Scraper turns Dice.com into clean, structured tech job data. Search by keyword, location and filters, or paste any Dice.com search or job URL, and get back title, company, recruiter or direct-hire type, parsed salary, location, workplace type, skills, posted date, an apply URL or email, and (optionally) the full job description with a structured address. Export straight to JSON, CSV or Excel, pull it through the API, or pipe results into the apps you already use.

### Why This Scraper?

- **Dual input.** Build a search from keywords, location and filters, or paste Dice.com URLs directly, both search pages and single job pages.
- **Full filter set.** Posted-date window, employment type, workplace type (remote/hybrid/on-site), employer type (recruiter vs direct hire), Easy Apply, visa-sponsor willingness, and sort by relevance or date.
- **Superset of fields.** Structured salary (min, max, currency, period), a skills list, an apply URL or apply email, a company profile link, and a structured street address, on top of every core listing field.
- **Forward pagination.** Walks the result set on its own, stopping naturally once a page repeats or empties, or once your listing cap is reached.
- **Optional detail enrichment.** Turn on richer per-job data (full description, skills, apply contact, structured address) only when you need it.
- **Built for schedules.** Incremental mode returns only new and changed listings on recurring runs, with clear, source-checked rules for when a listing can be marked gone and later reappear.
- **Export anywhere.** Optionally pipe results into Notion, Linear, Airtable or Apify itself, on top of the usual JSON, CSV or Excel download.

### Use Cases

- **Recruiters and staffing teams:** source active tech openings by skill, location and visa-sponsor willingness.
- **Tech job boards and aggregators:** mirror Dice.com listings into your own search or alert product.
- **Compensation and market research:** track posted salary ranges across roles, regions and employment types over time.
- **Career sites and newsletters:** build a "new today" feed from a saved search using incremental mode.
- **Workforce planning teams:** monitor recruiter-type or visa-sponsorship-willing postings for a given role or region.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `id` | `3fa41c9e7d2b4a11` |
| `guid` | `3fa41c9e-7d2b-4a11-9c3d-8b6f1e2a4d7c` |
| `url` | `https://www.dice.com/job-detail/3fa41c9e-7d2b-4a11-9c3d-8b6f1e2a4d7c` |
| `title` | `Senior Backend Engineer` |
| `company` | `Sample Company` |
| `companyProfileUrl` | `https://www.dice.com/company-profile/9c3d8b6f-1e2a-4d7c-aaaa-111122223333` |
| `city` / `state` / `country` | `Denver` / `Colorado` / `USA` |
| `locationDisplay` | `Denver, Colorado, USA` |
| `workplaceTypes` | `["Hybrid"]` |
| `remote` | `false` |
| `salary` | `USD 110,000.00 - 140,000.00 per year` |
| `salaryMin` / `salaryMax` | `110000` / `140000` |
| `salaryCurrency` / `salaryPeriod` | `USD` / `year` |
| `employmentType` | `Full-time` |
| `employerType` | `Recruiter` |
| `easyApply` | `true` |
| `willingToSponsor` | `false` |
| `skills` | `["Go", "Kubernetes", "PostgreSQL"]` |
| `postedDate` / `modifiedDate` | `2026-02-10T00:00:00Z` / `2026-02-12T00:00:00Z` |
| `validThrough` | `2026-03-12T00:00:00Z` |
| `applyType` | `APPLY_TO_EMAIL` |
| `applyUrl` / `applyEmail` | `https://careers.example.com/apply/4821` / `jobs@example.com` |
| `description` | `Full HTML job description appears here when Fetch detail pages is on.` |
| `companyReviews` | `null` |

Dice is a job board and does not expose company reviews or star ratings, so `companyReviews` is always `null`; company identity still comes through `company`, `companyProfileUrl`, and `companyLogo`. Newer fields: `jobLocation` (the structured location object), `workSetting`, `updated` (Dice's own last-updated timestamp), `descriptionHtml` (HTML copy of the description), `searchPage` (the result page the row came from) and `searchSortBy` (the sort in effect). Each record also carries `companyLogo` and `companyProfileId` (raw identifiers behind `companyProfileUrl`), `region`, `street` and `postalCode` (structured address, detail mode only), `positionId` and `score` (Dice's own listing id and the search's live relevance rank, which changes on every fetch and is therefore ignored by change detection, see "Resume and recurring updates"), `descriptionText` (a plain-text copy of `description`), `emails` (every email address found on the detail page, including the apply address), `workFromHomeAvailability`, `applicantLocationRequirements`, `searchQuery`/`searchLocation`/`page` (which keyword, location and result page produced the row), `sourceUrl`, and `scrapedAt`. With incremental mode on, every record additionally carries `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`, described below. `validThrough` is Dice's own stated expiration date for the posting and is only populated when detail is fetched; a change to it does trigger `UPDATED` like any other field, but it is never used to decide that a listing is gone (`EXPIRED`), which this actor's own change tracking only ever infers from whether a scan still finds the listing, not from this date.

#### Output views

The dataset has three views in the Console: **Listings** (every field), **Job details** (description, skills, structured address, apply contact and structured salary, the data that fills in with Fetch detail pages) and **Changes (incremental)** (`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt` next to the job identity).

### How to Use

1. Pick a **mode**: `search` (build from keywords, location and filters) or `url` (paste Dice.com search or job URLs).
2. Fill in the fields for that mode, and turn on **Fetch detail pages** if you want the full description, skills and apply contact.
3. Set **Max listings** (and, optionally, **Max pages**) to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Basic keyword search:**

```json
{
  "mode": "search",
  "queries": ["python developer"],
  "countryCode": "US",
  "maxListings": 20
}
```

**Search with location and filters:**

```json
{
  "mode": "search",
  "queries": ["engineer"],
  "location": "Austin, TX, USA",
  "workplaceType": "Remote",
  "postedDate": "SEVEN",
  "sortBy": "date",
  "maxListings": 50
}
```

**Search with detail enrichment (full description, skills, apply contact):**

```json
{
  "mode": "search",
  "queries": ["python developer"],
  "fetchDetails": true,
  "maxListings": 50
}
```

**URL mode (paste Dice.com URLs):**

The `urls` field is pre-filled with one example of every supported shape (listed below), so a URL-mode run started without editing it scrapes all of them, and `maxListings` still caps the total. Replace the list with your own URLs to scrape only those.

The `maxListings` cap defaults to 20 and accepts up to 100000 listings per run; `0` means no listing cap and the run then stops only at the natural end of the results (or at `maxPages` if set).

| URL shape | Example |
|---|---|
| Keyword search | `https://www.dice.com/jobs?q=python%20developer&countryCode=US` |
| Search with location, date filter and start page | `https://www.dice.com/jobs?q=python&location=Austin%2C%20TX%2C%20USA&page=2&filters.postedDate=ONE` |
| Filtered and sorted search | `https://www.dice.com/jobs?q=python&filters.workplaceTypes=Remote&filters.employmentType=FULLTIME&sort=posted` |
| Keyword landing page | `https://www.dice.com/jobs/q-python-jobs` |
| Keyword and location landing page | `https://www.dice.com/jobs/q-python-l-Austin%2C_TX-jobs` |
| City landing page | `https://www.dice.com/jobs-in-austin-tx` |
| State landing page | `https://www.dice.com/jobs-in-texas` |
| Single job page | `https://www.dice.com/job-detail/a47d1c79-25d6-4fba-be46-04290abe1864` |
| Apex host | `https://dice.com/jobs?q=python` |

City and state landing pages are read as the matching location search. Company profile pages and URLs from other sites are skipped with a warning in the run log. A page number in the URL starts the walk there (a partial scope), and `filters.*` and `sort` parameters in the URL are applied as written.

**URL mode with your own URL:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.dice.com/jobs?q=java&countryCode=US"
  ],
  "maxListings": 100
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/dice-com-scraper").call(run_input={"mode": "search", "queries": ["python developer"], "maxListings": 20})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], job["company"], job["salary"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/dice-com-scraper').call({ mode: 'search', queries: ['python developer'], maxListings: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Resume and recurring updates

There are two different things here, pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) reads the dedup key (`guid`) of every listing already saved by a prior run or dataset and skips those listings, so this run only appends what it has not already collected. It works cleanly for a pasted list of individual job URLs: each one is checked against the saved set independently, regardless of order. For a search, or a pasted search URL, be aware that the walk still starts at page 1 (or at the page number already in a pasted URL) on every run, and a page that comes back with zero listings beyond the already-saved set is treated as "the end of results." If a prior run already collected that same page, a plain resume of the same search can stop immediately after refetching page 1, without reaching further, newer listings deeper in the result set. To keep extending a large search-mode pull page by page, paste the search URL in URL mode with a `page` number that picks up after where the previous run stopped, rather than resuming the bare keyword search. An automatic same-run checkpoint separately reloads what this run has already saved after a platform migration or restart mid-run, with no input needed, so nothing already collected is re-pushed or re-billed; this is unrelated to `resumeFromRunId`, but it shares the same page-1-looks-like-the-end limitation described above, so a migrated search run can also stop earlier than it would have otherwise. A run also fails fast, before any scraping, if `resumeFromRunId` points to a run or dataset your account cannot read, or if it is combined with `incrementalMode` on a search that already has saved incremental state.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same* search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return `NEW` and `UPDATED` listings by default, plus `REAPPEARED` for any listing a previous run had already marked gone. A listing is only ever marked gone at all, and can therefore only ever later show up as `REAPPEARED`, once some earlier run had `emitExpired` switched on and completed a full, uncapped scan of the tracked search without finding it; a run that never turns `emitExpired` on never marks anything gone, so `REAPPEARED` cannot occur under that history. Duplicates and truly unchanged listings are suppressed by default (and not charged, including the detail-enrichment surcharge). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned, and billed for. State is isolated automatically per mode/keyword/location/filter/URL and `fetchDetails` setting; set `stateKey` to name or deliberately share a monitoring campaign. Raising `maxPages`, `maxListings`, or `maxNotifyListings` never starts a new baseline, those are run-size caps, not part of the tracked search.

**What counts as a change.** Every output field is compared except `scrapedAt`, `score`, the order of `skills`, `workplaceTypes` and `emails` (a reorder is not a change) and the logo host and query string (only the logo path is compared). A changed title, salary, company, description, dates, apply data or any other field makes a listing `UPDATED`, and `changedFields` names it. Add your own fields to `ignoreFieldsForChanges` (a dotted path reaches a nested field); they stay in the output but are not compared, and changing that list never produces a wave of `UPDATED` rows. A listing is only marked `EXPIRED` after a complete scan from page 1; a pasted search URL that starts at page 2 or later is a partial scan and never expires anything.

Scheduled-run example, same search input (`{ "mode": "search", "queries": ["python developer"], "incrementalMode": true }`) run daily: on day 1, the first run ever for this search, every listing comes back with `"changeType": "NEW"`. On day 2, with the schedule firing again on the identical input and `emitExpired` left off, listings whose title, salary, company, description or other tracked field changed come back as `"changeType": "UPDATED"` with `changedFields` listing what changed; brand-new postings come back as `"changeType": "NEW"`; listings that are still there, unchanged, are **not** returned at all unless `emitUnchanged` is on. `REAPPEARED` and `EXPIRED` never appear on this schedule, because `emitExpired` was never turned on, so no listing was ever marked gone in the first place.

##### What counts as "changed"

Change detection compares every real listing field. Two fields are deliberately excluded because they are not real listing data:

- `scrapedAt`, a per-run timestamp stamped fresh on every push. Comparing it would make every re-scrape look "changed."
- `score`, the search's live relevance rank. Measured live: fetching the same search page twice a few seconds apart changed `score` on every common listing while every other field stayed identical. It moves with the query and the rest of the index, not with the listing itself.

Everything else, including `title`, `company`, `salary`/`salaryMin`/`salaryMax`, `description`, `skills`, `employmentType`, `postedDate`, `modifiedDate`, `validThrough`, `easyApply`, and the location fields, is real data and does trigger `UPDATED` when it changes.

##### Cap semantics under incremental mode

`maxListings` (and each search's share of it) counts listings **scanned**, not listings **emitted**. Outside incremental mode the two numbers are always equal, since nothing is suppressed. Under incremental mode, with most listings `UNCHANGED`, counting emitted rows instead would make a quiet run walk deeper and deeper pages trying to "backfill" the cap with rows it would just suppress again, so cost would never drop even when nothing changed. Counting scanned listings means a quiet incremental run costs the same, in pages walked, as the equivalent full run, and stays there.

`emitExpired` only fires after a **complete** scan of the tracked search, never on a run capped by `maxListings`, a `resumeFromRunId` run, or a run that only fetched individual job detail URLs (those never walk a catalogue page, so there is no way to tell "gone" apart from "not reached"). Turning `emitExpired` on is also what marks a listing gone at all, so it is the only way a later run's `REAPPEARED` classification can ever fire for that listing. A partial scan leaves previously-tracked listings' state untouched instead of guessing.

### Send results into your apps (MCP connectors)

Optionally push each result into the tools you already use, through Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape; the Apify dataset itself is never changed. Authorize a connector once under Apify, Settings, Integrations, then select it in the "Pipe results into your apps" input field. For Notion, also set the Notion parent page. The connector receives a condensed, human-readable summary per item (a title plus its key fields flattened to plain text); the complete record always stays in the Apify dataset. Leave the field empty to skip. Supported connectors include Notion, Linear, Airtable, and Apify.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` builds a query from the fields below; `url` reads the URLs list. |
| `queries` | array | prefill: `["python developer"]` | Free-text keywords. One scrape per keyword. Leave empty to browse without a keyword filter. |
| `location` | string | prefill: (empty) | City + state or country. Leave empty to search nationwide. |
| `radius` | integer | `0` | Distance around the location to include (0 to 500). `0` uses the site default. Ignored when Location is empty. |
| `radiusUnit` | string | `mi` | `mi` or `km`. |
| `countryCode` | string | `US` | Two-letter country code for the job market. |
| `postedDate` | string | `any` | `any`, `ONE`, `THREE`, or `SEVEN` (days). |
| `employmentType` | string | `any` | `FULLTIME`, `PARTTIME`, `CONTRACTS`, `THIRD_PARTY`. |
| `workplaceType` | string | `any` | `Remote`, `Hybrid`, `On-Site`. |
| `employerType` | string | `any` | `Recruiter` or `Direct Hire`. |
| `easyApply` | boolean | `false` | Only jobs with one-click Easy Apply. |
| `willingToSponsor` | boolean | `false` | Only jobs whose employer indicates willingness to sponsor a visa. |
| `sortBy` | string | `relevance` | `relevance` (site default) or `date` (newest first). |
| `urls` | array | one example of each supported URL shape | One or more Dice.com URLs (search pages, landing pages or single job pages). Filter fields above are ignored in this mode. |
| `fetchDetails` | boolean | `false` | Also fetch each job's detail page for the full HTML description, skills, structured address, apply URL/email, valid-through date, and structured salary. A pasted single job URL (URL mode) always fetches and is billed for its detail page regardless of this toggle, since reading that one job is only possible by fetching it. |
| `maxPages` | integer | `0` | Safety bound on result pages per search (roughly 20 listings per page). `0` = unlimited: the walk stops once a page comes back empty or repeats listings already collected, or sooner once Max listings is hit. |
| `maxListings` | integer | `20` | The sole soft cap on the run, across all searches. `0` = unlimited (bounded only by the natural stop on Max pages). |
| `proxy` | object | prefill: Apify Proxy | Connection routing. |
| `mcpConnectors` | array | (none) | Optional MCP connectors to also receive a summary of each result. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |
| `resumeFromRunId` | string | (none) | Optional prior run ID or dataset ID. Listings already saved there are loaded before scraping starts and skipped, so this run only appends new listings. See "Resume and recurring updates" above. |
| `incrementalMode` | boolean | `false` | Daily/recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Resume and recurring updates" above. |
| `stateKey` | string | (none) | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from the search/URL and filter settings when left empty. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return listings unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return listings from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search (not capped, not a resume, not detail-URLs-only). This is also what marks a listing gone at all, which is what later allows a `REAPPEARED` classification. Adds and bills extra synthetic rows. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Field names (dotted path for a nested field) that never make a listing `UPDATED`; they stay in the output. `scrapedAt` and `score` are always ignored. Changing the list causes no wave of `UPDATED` rows. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live listing.

```json
{
  "id": "3fa41c9e7d2b4a11",
  "guid": "3fa41c9e-7d2b-4a11-9c3d-8b6f1e2a4d7c",
  "url": "https://www.dice.com/job-detail/3fa41c9e-7d2b-4a11-9c3d-8b6f1e2a4d7c",
  "title": "Senior Backend Engineer",
  "company": "Sample Company",
  "companyProfileUrl": "https://www.dice.com/company-profile/9c3d8b6f-1e2a-4d7c-aaaa-111122223333",
  "city": "Denver",
  "state": "Colorado",
  "country": "USA",
  "locationDisplay": "Denver, Colorado, USA",
  "workplaceTypes": ["Hybrid"],
  "remote": false,
  "salary": "USD 110,000.00 - 140,000.00 per year",
  "salaryMin": 110000,
  "salaryMax": 140000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "employmentType": "Full-time",
  "employerType": "Recruiter",
  "easyApply": true,
  "willingToSponsor": false,
  "skills": ["Go", "Kubernetes", "PostgreSQL"],
  "positionId": "R-048213",
  "score": 812.4,
  "postedDate": "2026-02-10T00:00:00Z",
  "modifiedDate": "2026-02-12T00:00:00Z",
  "validThrough": "2026-03-12T00:00:00Z",
  "applyType": "APPLY_TO_EMAIL",
  "applyUrl": null,
  "applyEmail": "jobs@example.com",
  "emails": ["jobs@example.com"],
  "summary": "Sample summary text appears here.",
  "description": "<p>Full HTML description appears here when Fetch detail pages is on.</p>",
  "companyReviews": null,
  "scrapedAt": "2026-02-12T08:00:00.000Z"
}
```

With `incrementalMode` on, every record additionally carries `changeType` (`NEW`/`UPDATED`/`UNCHANGED`/`REAPPEARED`/`EXPIRED`), `changedFields` (array, populated only for `UPDATED`), `firstSeenAt`, and `lastSeenAt`, see "Resume and recurring updates" above.

### Plan Requirement

The default connection (Apify datacenter proxy) is enough for Dice.com: no residential proxy is required and no special setup is needed. Dice.com served plain datacenter traffic without a challenge in our checks, but that check was a small sample from our own datacenter connection, so heavy or frequent runs can still meet limits. If the default connection is refused, the run switches to a backup connection and logs: "Served over the backup connection (the default connection was unavailable)." Apify free plans do not include residential proxies; if you pick the residential group on such a plan, the run logs a warning ("The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable.") and carries on over the Apify datacenter connection. If every connection is refused, no warning about continuing is shown: the run stops with the message that the proxy connection was refused for this account and a paid plan is required. For large or frequent runs the residential group gives more headroom; pick it under Proxy & connection, or supply your own proxy URLs.

### FAQ

#### How much does it cost?

You pay per job listing returned, a smaller surcharge (the detail-enrichment event, one unit per listing) for each listing where detail enrichment actually fetched data, and a small per-run start fee. The detail-enrichment charge is made per item, right after that listing is saved to the dataset, so a run stopped early is billed only for what was delivered; listings that are not saved to the dataset (unchanged listings suppressed in incremental mode, or failed fetches) are never charged for detail. When **Emit unchanged** is on, each emitted enriched row is charged once, like any other returned listing. A pasted single job URL (URL mode) is billed for its detail page every time it succeeds, regardless of the Fetch detail pages toggle, because reading that one job requires fetching it. The **Pricing** tab shows the current rates. Use **Max listings** to cap the cost of any run.

#### Is it legal to scrape Dice.com?

This actor collects only publicly available job listing data. You are responsible for how you use it: follow Dice's terms and the laws that apply to you, and get legal advice before any commercial redistribution, especially since some listings carry a recruiter's personal contact details.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and changed listings for that saved search, and unchanged ones are not billed unless **Emit unchanged** is on.

#### Why did Resume return almost nothing on my second run of the same search?

Resume continues one specific interrupted or previous crawl by skipping listings a prior run already saved; it is not a way to poll a search for "what's new" (use **Incremental mode** for that). It works best for a pasted list of individual job URLs, checked one by one. For a plain search, Resume restarts at page 1 every time and stops as soon as a page returns nothing beyond what was already saved, so if the prior run already covered that page, this run can stop immediately instead of walking further. To keep extending a large search-mode pull, paste the search URL in URL mode with a `page` number that picks up where the previous run left off.

#### Why did my run FAIL instead of finishing with an empty dataset?

A run that could not load a single results page is marked **Failed**, never Succeeded with zero rows, so a refused connection cannot be mistaken for "nothing matched your search". The run log and the status message give one of three reasons:

- "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid."
- "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Dice pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
- "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

A search that loaded pages normally but simply had no matching jobs finishes as Succeeded with an empty dataset. A run also stops with an error before any scraping if the connection is set to no proxy at all (Apify Proxy off without your own proxy URLs); an unrecognized proxy group falls back to the default connection.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/totaljobs-com-scraper"><b>Totaljobs Scraper</b></a><br>Scrape UK job listings from Totaljobs.com by keyword, location, filters, or URL. Extract...</td><td>💼 <a href="https://apify.com/abotapi/ziprecruiter-com-scraper"><b>ZipRecruiter Scraper</b></a><br>Scrape ZipRecruiter jobs with titles, companies, salaries, locations, benefits, apply...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/monster-com"><b>Monster Jobs Scraper</b></a><br>Scrape Monster.com job listings from search and direct job URLs. Extract full...</td><td>💼 <a href="https://apify.com/abotapi/hiring-cafe-scraper"><b>Hiring.Cafe Jobs</b></a><br>Unlock powerful Hiring.Cafe job data extraction. Use advanced filters or URLs to get 100+...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/caterer-com-scraper"><b>Caterer.com Scraper</b></a><br>Scrape UK hospitality jobs from Caterer.com, including chef, hotel, restaurant, bar, and...</td><td>💼 <a href="https://apify.com/abotapi/nijobs-scraper"><b>NIJobs Scraper</b></a><br>Scrape NIJobs.com listings across Northern Ireland by keyword, location, salary, posted...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/dice-com-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Search mode = pick keywords + location + filters below. URL mode = paste any dice.com search or job URL refined in your browser.

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

Free-text search terms (e.g. "python developer", "data engineer", "devops"). One scrape per keyword. Leave empty to browse without a keyword filter.

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

City + state or country. Examples: "New York, NY, USA", "Austin, TX, USA". Leave empty to search nationwide.

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

Distance around the location to include. Leave at 0 to use the site default radius. Ignored when Location is empty.

## `radiusUnit` (type: `string`):

Unit for the search radius.

## `countryCode` (type: `string`):

Two-letter country code for the job market (Dice is primarily US). Example: "US".

## `postedDate` (type: `string`):

Only return jobs posted within this window. "Any time" walks the unfiltered results.

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

Filter by employment scope. "Any" walks the unfiltered results.

## `workplaceType` (type: `string`):

Filter by on-site / hybrid / remote arrangement. "Any" walks the unfiltered results.

## `employerType` (type: `string`):

Filter by who posted the job. "Any" walks the unfiltered results.

## `easyApply` (type: `boolean`):

Only return jobs that support one-click Easy Apply.

## `willingToSponsor` (type: `boolean`):

Only return jobs whose employer indicates willingness to sponsor a visa.

## `sortBy` (type: `string`):

Result ordering. "Relevance" is the site default; "Date" surfaces the most recently posted first.

## `urls` (type: `array`):

One or more dice.com URLs: a /jobs search (any filters, sort or page), a keyword or location landing page (/jobs/q-python-jobs, /jobs-in-austin-tx), or a single /job-detail/<id> job page. Left untouched, the field scrapes one example of each supported shape. Company profile pages and other sites are skipped with a warning. Multi-URL supported; each URL pages forward independently. Filter fields above are ignored.

## `fetchDetails` (type: `boolean`):

When ON, also fetch each job's detail page for the full HTML description, skills list, structured street address, apply URL/email, valid-through date, and structured salary. Search results already carry id, title, company, location, salary, employment type, posted date, and summary, so many feeds work great with this OFF. Default OFF.

## `maxPages` (type: `integer`):

Safety bound on how many result pages to walk per keyword (or per pasted URL). Each page returns roughly 20 listings. 0 (default) means unlimited - the walk stops naturally once a page comes back empty or repeats listings already collected, or sooner once Max listings is reached. Set a number only for an explicit page cap below that natural stop.

## `maxListings` (type: `integer`):

The sole soft cap on the run: stop after this many listings across all searches. Defaults to 20 so a run stays small unless you raise it. 0 = unlimited (bounded only by the natural stop on Max pages).

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

Connection routing. The default works on any Apify plan, including the free tier. Residential routing is optional and can improve reliability under heavy use.

## `resumeFromRunId` (type: `string`):

Optional. Paste a previous run ID or dataset ID to continue a large walk-all pull: listings already saved by that run are loaded before scraping starts and skipped, so this run only appends new listings. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching listings as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings. Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately for each mode/keyword/location/filter/URL and detail-fetch setup; use State key when you want to name or deliberately share a monitoring campaign. Change detection compares every output field except scrapedAt, score (search relevance rank, changes on every fetch), the order of skills/workplaceTypes/emails, and the logo host/query; a changed title, salary, company, description, dates, apply data or any other field makes a listing UPDATED with changedFields naming it. Add more fields to ignore with "Ignore fields when detecting changes". A listing is only marked EXPIRED after a complete, uncapped scan from page 1. To continue one specific interrupted run instead, use Resume from a prior run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/URL and filter settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return listings that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max listings capped it, when Resume was used, or on a run that only fetched individual job detail URLs. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Field names that must NOT make a listing UPDATED, for example `modifiedDate` or `summary`. Use a dotted path for a nested field. The fields stay in the output; they are just not compared. scrapedAt and score are always ignored. Changing this list never causes a wave of UPDATED rows.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "python developer"
  ],
  "location": "",
  "radius": 0,
  "radiusUnit": "mi",
  "countryCode": "US",
  "postedDate": "any",
  "employmentType": "any",
  "workplaceType": "any",
  "employerType": "any",
  "easyApply": false,
  "willingToSponsor": false,
  "sortBy": "relevance",
  "urls": [
    "https://www.dice.com/jobs?q=python%20developer&countryCode=US",
    "https://www.dice.com/jobs?q=python&location=Austin%2C%20TX%2C%20USA&page=2&filters.postedDate=ONE",
    "https://www.dice.com/jobs?q=python&filters.workplaceTypes=Remote&filters.employmentType=FULLTIME&sort=posted",
    "https://www.dice.com/jobs/q-python-jobs",
    "https://www.dice.com/jobs/q-python-l-Austin%2C_TX-jobs",
    "https://www.dice.com/jobs-in-austin-tx",
    "https://www.dice.com/jobs-in-texas",
    "https://www.dice.com/job-detail/a47d1c79-25d6-4fba-be46-04290abe1864",
    "https://dice.com/jobs?q=python"
  ],
  "fetchDetails": false,
  "maxPages": 0,
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `changes` (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 = {
    "mode": "search",
    "queries": [
        "python developer"
    ],
    "location": "",
    "urls": [
        "https://www.dice.com/jobs?q=python%20developer&countryCode=US",
        "https://www.dice.com/jobs?q=python&location=Austin%2C%20TX%2C%20USA&page=2&filters.postedDate=ONE",
        "https://www.dice.com/jobs?q=python&filters.workplaceTypes=Remote&filters.employmentType=FULLTIME&sort=posted",
        "https://www.dice.com/jobs/q-python-jobs",
        "https://www.dice.com/jobs/q-python-l-Austin%2C_TX-jobs",
        "https://www.dice.com/jobs-in-austin-tx",
        "https://www.dice.com/jobs-in-texas",
        "https://www.dice.com/job-detail/a47d1c79-25d6-4fba-be46-04290abe1864",
        "https://dice.com/jobs?q=python"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/dice-com-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 = {
    "mode": "search",
    "queries": ["python developer"],
    "location": "",
    "urls": [
        "https://www.dice.com/jobs?q=python%20developer&countryCode=US",
        "https://www.dice.com/jobs?q=python&location=Austin%2C%20TX%2C%20USA&page=2&filters.postedDate=ONE",
        "https://www.dice.com/jobs?q=python&filters.workplaceTypes=Remote&filters.employmentType=FULLTIME&sort=posted",
        "https://www.dice.com/jobs/q-python-jobs",
        "https://www.dice.com/jobs/q-python-l-Austin%2C_TX-jobs",
        "https://www.dice.com/jobs-in-austin-tx",
        "https://www.dice.com/jobs-in-texas",
        "https://www.dice.com/job-detail/a47d1c79-25d6-4fba-be46-04290abe1864",
        "https://dice.com/jobs?q=python",
    ],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/dice-com-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 '{
  "mode": "search",
  "queries": [
    "python developer"
  ],
  "location": "",
  "urls": [
    "https://www.dice.com/jobs?q=python%20developer&countryCode=US",
    "https://www.dice.com/jobs?q=python&location=Austin%2C%20TX%2C%20USA&page=2&filters.postedDate=ONE",
    "https://www.dice.com/jobs?q=python&filters.workplaceTypes=Remote&filters.employmentType=FULLTIME&sort=posted",
    "https://www.dice.com/jobs/q-python-jobs",
    "https://www.dice.com/jobs/q-python-l-Austin%2C_TX-jobs",
    "https://www.dice.com/jobs-in-austin-tx",
    "https://www.dice.com/jobs-in-texas",
    "https://www.dice.com/job-detail/a47d1c79-25d6-4fba-be46-04290abe1864",
    "https://dice.com/jobs?q=python"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/dice-com-scraper --silent --output-dataset

```

## MCP server setup

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