# SimplyHired Scraper - Jobs, Salaries & Company Details (`abotapi/simplyhired-com-scraper`) Actor

Scrape active SimplyHired jobs across the US, UK, Canada, Australia, Ireland and India from $1/1K listings. Search by filters or URL. Extract 40+ fields including job titles, companies, salary ranges, full HTML descriptions, qualifications, coordinates and apply URLs.

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

## Pricing

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

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

## SimplyHired Jobs Scraper: Titles, Salaries, Full Descriptions & Apply Links

SimplyHired Jobs Scraper turns SimplyHired into a clean job-postings API. Get job title, company, employer rating, parsed salary range (min, max, currency, period), city, state, latitude/longitude, work setting, qualifications, benefits, the full HTML job description and the SimplyHired apply link for active listings across the US, UK, Canada, Australia, Ireland and India sites. Build a search from keywords, location, job type and country, or paste any simplyhired.com /search or /job URL, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **40+ fields per job ad.** Includes a parsed salary range (min, max, currency, period), city, state, latitude/longitude, work setting, qualifications, benefits and the full HTML description, not just the 10 to 15 fields typical scrapers return.
- **Two ways to search.** Build with keywords, location, job type and country site, or paste any simplyhired.com /search URL and the scraper walks pagination forward from there, or a /job URL for that single listing.
- **Six regional sites.** One input field covers United States, United Kingdom, Canada, Australia, Ireland and India.
- **Detail enrichment.** Concurrent detail-page enrichment with per-record delivery, so a mid-page failure still keeps the listings already fetched instead of losing the whole page.
- **No duplicates.** Client-side de-duplication across pages and searches by the canonical listing id, with the listing budget split evenly across multiple keywords in one run.
- **Connection fallback.** Automatic retry across several connection routes when one is challenged or refused, and a clear failure message (never a silently empty result) when no page could be read.
- **Built for schedules.** Incremental mode returns only new and changed listings on recurring runs, and Resume continues one interrupted run without re-collecting what it already saved.

### Use Cases

- **Job boards and aggregators:** pull fresh SimplyHired listings by keyword and location to mirror or enrich your own job feed.
- **Recruiter and sourcing tools:** track new postings for target companies or roles as soon as they appear, and pull only what changed on a schedule.
- **Compensation research:** build salary-range datasets by title, location and job type from the parsed `salaryMin` / `salaryMax` fields.
- **Labor market analytics:** measure posting volume, remote-vs-onsite mix and qualifications trends across regions and countries.
- **Application tooling:** collect the SimplyHired apply link and full job description text to feed an autofill, alerting or matching tool.

### Data You Get

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

| Field | Example |
|---|---|
| `id` | `"abcd1234example"` |
| `url` | `"https://www.simplyhired.com/job/abcd1234example"` |
| `title` / `displayTitle` | `"Software Engineer"` / `"Software Engineer, Backend"` |
| `company` | `"Sample Company Inc"` |
| `companyRating` | `4.2` |
| `companyPageUrl` | `"https://www.simplyhired.com/company/000-sample-company-inc"` |
| `location` / `formattedLocation` | `"New York, NY"` |
| `city` / `state` | `"New York"` / `"NY"` |
| `latitude` / `longitude` | `40.7128` / `-74.006` |
| `country` | `"us"` |
| `jobTypes` / `workSettings` | `["Full-time"]` / `["Hybrid"]` |
| `salary` | `"$140,000 - $180,000 a year"` |
| `salaryMin` / `salaryMax` | `140000` / `180000` |
| `salaryCurrency` / `salaryPeriod` | `"USD"` / `"year"` |
| `descriptionHtml` / `descriptionText` | full HTML job description / the same description as plain text |
| `qualifications` | `["Bachelor's degree", "5+ years experience"]` |
| `benefits` | `["Health insurance", "401(k)"]` |
| `skillsAll` | `["Python", "AWS", "API design"]` |
| `datePosted` | `"2026-09-20T08:00:00.000Z"` |
| `applyUrl` | the SimplyHired apply link, a redirect that forwards to the employer's application page (the token rotates on every fetch) |
| `sponsored` | `false` |
| `source` | `"SimplyHired"` |
| `changeType` (incremental mode only) | `"NEW"` (also `"UPDATED"`, `"UNCHANGED"`, `"REAPPEARED"`, `"EXPIRED"`) |

Each record also carries `normalizedTitle` (a lowercased title for matching), `employerLogoUrl` and `employerHomepage`, `remoteAttributes` (SimplyHired's own remote-work tags), the raw `compensation` object behind the parsed salary fields, a short `snippet` preview, a `requirements` list distinct from `qualifications`, `datePostedEpochMs` and `dateOnIndeed` (millisecond timestamps), `mobileApplyUrl` alongside `applyUrl`, `indeedApply` / `auction` / `expired` flags, `jobCardTrackingKey` (SimplyHired's own per-render analytics key), the `jobTypeRequested` / `searchQuery` / `searchLocation` / `pageNumber` that produced the row, `scrapedAt`, and migration-friendly names: `sourceSearchUrl` (the search page the row came from), `summary` (the snippet, or the start of the description), `jobKey` (same as `id`), `skills` (same as `skillsAll`) and `jobType` (`jobTypes` joined as text). These five are left out of change detection, so they never trigger an UPDATED row. In incremental mode, `changedFields`, `firstSeenAt` and `lastSeenAt` are added alongside `changeType`.

### How to Use

1. Pick a **mode**: `search` (build with keywords, location, job type and country) or `url` (paste one or more simplyhired.com links).
2. For search mode, fill in keywords, location, country site and job type. For URL mode, paste any `/search` or `/job` URL; the search-builder fields above are ignored.
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.

**Search by keyword and location:**

```json
{
  "mode": "search",
  "queries": ["software engineer"],
  "location": "Remote",
  "country": "us",
  "jobType": "fulltime",
  "maxPages": 5,
  "maxListings": 100,
  "fetchDetails": true
}
```

**Multiple keywords, sponsored cards skipped, SERP-only fields for a fast and cheap feed:**

```json
{
  "mode": "search",
  "queries": ["nurse", "physical therapist", "medical assistant"],
  "location": "Boston, MA",
  "country": "us",
  "jobType": "any",
  "skipSponsored": true,
  "maxListings": 150,
  "fetchDetails": false
}
```

#### URL shapes (URL mode)

The `q` (keyword), `l` (location), `jt` (job type) and `pn` (start page) values in a pasted link are applied as given. A link starting at `pn=2` or higher is a partial scan and never produces `EXPIRED` rows in incremental mode. Links on other hosts, or on an unsupported SimplyHired domain, are skipped with a warning in the log.

| Shape | Example |
| --- | --- |
| Keyword + location | `https://www.simplyhired.com/search?q=software+engineer&l=Remote` |
| Paginated (starts at page `pn`) | `https://www.simplyhired.com/search?q=nurse&l=Boston%2C+MA&pn=2` |
| Job-type filter (`jt`) | `https://www.simplyhired.com/search?q=engineer&l=Remote&jt=CF3CP` |
| Keyword only | `https://www.simplyhired.com/search?q=nurse` |
| Location only | `https://www.simplyhired.com/search?l=Remote` |
| Search with a preselected `job=` | `https://www.simplyhired.com/search?q=engineer&l=Remote&job=zzz` |
| Single job page | `https://www.simplyhired.com/job/mo_LRS0yJ90r4G7Bi9HsIQfPAspEk3xIq65MtLP0wZaC8PKRelCDBQ` |
| Regional sites (.co.uk, .ca, .com.au, .ie, .co.in) | `https://www.simplyhired.co.uk/search?q=nurse&l=London` |

**URL mode on the UK site, mixing a search link and a single job link:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.simplyhired.co.uk/search?q=product+manager&l=London",
    "https://www.simplyhired.com/job/abcd1234example"
  ],
  "maxPages": 5,
  "maxListings": 200,
  "fetchDetails": true
}
```

**Recurring monitoring (scheduled daily run, only what changed):**

```json
{
  "mode": "search",
  "queries": ["software engineer"],
  "location": "Remote",
  "country": "us",
  "maxListings": 0,
  "fetchDetails": true,
  "incrementalMode": true,
  "emitExpired": true
}
```

Schedule this same input to run daily. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED` and `EXPIRED` listings (plus `REAPPEARED` once a listing that was previously marked `EXPIRED` shows up again), not the full result set every time.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/simplyhired-com-scraper").call(run_input={
    "mode": "search",
    "queries": ["software engineer"],
    "location": "Remote",
    "country": "us",
})
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/simplyhired-com-scraper').call({
    mode: 'search',
    queries: ['software engineer'],
    location: 'Remote',
    country: 'us',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Max pages, Max listings and the per-keyword budget

**Max pages** caps how many SERP pages are walked per search entry (or per pasted URL); each page returns up to 20 listings. Leave it empty to walk every page until the site's own end-of-results signal. **Max listings** caps the total across every search in the run; set it to `0` for no cap. When a run has multiple keywords, the Max listings budget is split evenly across them so one keyword can't crowd out the others.

#### Resume and recurring updates

Two different things, both under one input section, so they don't get confused:

- **Resume from a previous run** (`resumeFromRunId`) continues **one specific interrupted run**. Paste the ID of a previous run (or its dataset), and this run skips every listing `id` already saved there, appending only new ones. Good for an unbounded pull (`maxPages` left empty, `maxListings: 0`) that got cut off.
- **Incremental mode** (`incrementalMode`) is for **recurring monitoring of the SAME search** (for example, scheduled daily). Turn it on and the actor remembers the previous run's results itself, no run or dataset id to paste. Each listing in the output then gets a `changeType`:

  - `NEW`: never tracked before for this search.
  - `UPDATED`: tracked before, but at least one non-volatile field changed since last time (see `changedFields`). Fields that change on every scrape are never compared: `scrapedAt`, `applyUrl`, `mobileApplyUrl`, `jobCardTrackingKey`, `pageNumber` and `auction` stay in the output but never trigger a false `UPDATED`; so do `jobKey`, `summary`, `skills`, `jobType` and `sourceSearchUrl`, which are copies or derivations of compared fields (`id`, `snippet` / `descriptionText`, `skillsAll`, `jobTypes` and your search inputs), so a real change still shows through the source field. A plain field name is ignored at any depth, including inside nested objects. Add more with `ignoreFieldsForChanges` (a plain name is ignored at any depth, a dotted path such as `compensation.min` ignores that exact nested field); widening the list never causes a one-time wave of `UPDATED` rows. A run that starts from a pasted URL with `pn=2` or higher is a partial scan and never produces `EXPIRED`.
  - `UNCHANGED`: identical to last time. Only emitted (and billed) when `emitUnchanged` is on; off by default.
  - `EXPIRED`: was tracked as present in a previous run, and was not found in this one. Only produced when `emitExpired` is on **and** this run reached the natural end of results for every tracked search, with no `maxListings` cap hit and no `resumeFromRunId`. A capped, blocked or resumed run cannot tell "gone" apart from "not reached yet", so it leaves the previous state untouched instead of guessing.
  - `REAPPEARED`: a listing marked `EXPIRED` in an earlier run that has now been found again. This can only happen for a listing that some *earlier* run actually marked absent, which itself only happens when that earlier run had `emitExpired` on and completed a full, uncapped, non-resumed scan of the tracked search. If `emitExpired` has never been on, or every run so far was capped, blocked, or used `resumeFromRunId`, no listing is ever marked absent, so `REAPPEARED` cannot occur, no matter how many times a listing actually disappears and comes back.

  State is tracked separately per query/location/country/jobType/skipSponsored/URL-set/`fetchDetails` combination, or under one shared campaign if you set `stateKey`, so two differently-configured runs never mix baselines.

Independently of both, a checkpoint is saved automatically as the run progresses. If the run is interrupted by an Apify platform migration or you click Resurrect on a failed run, it picks back up in the *same* run without re-saving, or re-charging for, listings it already collected. No input is needed for this; it's automatic.

#### Output tabs and run summary

The Output tab offers three views of the same listings: `overview` (the table), `detail` (full description, qualifications, benefits, skills, requirements and the salary breakdown) and `changes` (incremental rows: change type, changed fields, first/last seen). Runs with more than one keyword or URL also save a run summary under the key `OUTPUT` in the key-value store: for each keyword or URL the pages loaded, the listings delivered and a status (`completed`, `partial`, `unreachable` or `skipped`; a `/job` URL whose listing an earlier entry already delivered is `completed` with 0 listings).

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

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape; the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search` (use the builder fields below) or `url` (paste SimplyHired URLs). |
| `queries` | array | no default (prefill: `["software engineer"]`) | One scrape per keyword. Leave empty to browse without a keyword filter. |
| `location` | string | no default (prefill: `Remote`) | City, state, ZIP, or "Remote". Leave empty to search nationwide. |
| `country` | string | `us` | Which SimplyHired regional site to query: `us`, `uk`, `ca`, `au`, `ie`, `in`. |
| `jobType` | string | `any` | `any` / `fulltime` / `parttime` / `contract` / `temporary` / `internship` / `commission`. |
| `skipSponsored` | boolean | `false` | Drop sponsored / promoted job cards. Applied client-side, in both modes. |
| `datePosted` | string | `any` | `1`, `3`, `7` or `14` days. Sent to the site as a native filter in Search mode and also checked against each row's post date (both modes). |
| `minSalary` | integer | `0` | Minimum yearly pay; hourly, weekly and monthly pay is annualised and jobs with no listed pay are dropped. Native filter in Search mode, also checked on this side (both modes). |
| `remoteOnly` | boolean | `false` | Keep only jobs flagged remote. Applied client-side, in both modes. |
| `sortBy` | string | `relevance` | `date` orders the listings of each fetched page newest first, client-side only: the site ignores its own sort parameters, so this does not re-order across pages. |
| `keyword` | string | none | Alias for `queries`, so inputs written for other SimplyHired scrapers run unchanged. |
| `startUrls` | array | none | Alias for `urls`; replaces the 7 sample URLs and switches to URL mode when no keywords are given. A `urls` list you edited yourself takes priority. |
| `results_wanted` | integer | none | Alias for `maxListings`; used when `maxListings` is unset or still at its default of 20. |
| `max_pages` | integer | none | Alias for `maxPages`. |
| `urls` | array | 7 sample URLs, one per shape (see the URL shapes table) | URL mode only. A /search URL is paged forward independently; a /job URL returns that one listing. Keyword, location, job type and start page come from the pasted URL; `datePosted`, `minSalary`, `remoteOnly`, `sortBy` and `skipSponsored` are still applied on this side to every URL's results. If you switch to URL mode and leave this untouched, those 7 sample URLs are scraped; replace them with your own. |
| `maxPages` | integer | no default (empty = unlimited) | Max SERP pages per search (or per pasted URL). Each page returns up to 20 listings. Leave empty to walk every result page until the site's own end-of-results signal. |
| `maxListings` | integer | `20` | Stop after this many listings across all searches. `0` = no cap. |
| `fetchDetails` | boolean | `true` | Also fetch each job's detail page for the full HTML description, qualifications list, employer logo, lat/lng and apply URL. |
| `resumeFromRunId` | string | no default (empty) | A previous run ID or dataset ID to continue one specific interrupted run. Listings already collected there (matched by `id`) are skipped. |
| `incrementalMode` | boolean | `false` | Turn on for daily or recurring monitoring of the same search. See "Resume and recurring updates" above. |
| `stateKey` | string | no default (empty) | Incremental mode only. Names the monitoring campaign so its state stays stable, or lets you deliberately share state across differently-configured runs. Leave empty to derive a key automatically from the query/location/URL and detail settings. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) listings that haven't changed, marked `UNCHANGED`. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Extra fields that never count as a change (plain name = any depth, dotted path = exact nested field). They are still returned in the output. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) listings from a previous run that are no longer found, marked `EXPIRED`. Only produced after a run that fully scanned the tracked search. |
| `proxy` | object | no default (prefill: Apify Residential, country `US`) | SimplyHired is behind a Cloudflare challenge. The prefill is Apify Residential pinned to country `US`; the actor escalates through further routes automatically when a page is challenged. For other regional sites, change the country to match (`GB` / `CA` / `AU` / `IE` / `IN`). |
| `mcpConnectors` | array | no default (empty) | Optional: send a summary of each listing to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | no default (empty) | Notion connector only: page under which listing pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live ad. `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt` are only added when `incrementalMode` is on; a normal run's output shape below is unchanged.

```json
{
  "id": "abcd1234example",
  "url": "https://www.simplyhired.com/job/abcd1234example",
  "title": "Software Engineer",
  "displayTitle": "Software Engineer, Backend",
  "normalizedTitle": "software engineer",
  "company": "Sample Company Inc",
  "companyRating": 4.2,
  "companyPageUrl": "https://www.simplyhired.com/company/000-sample-company-inc",
  "employerLogoUrl": "https://logos.example-cdn.com/sample-company-inc.png",
  "employerHomepage": "https://www.samplecompany.example",
  "location": "New York, NY",
  "formattedLocation": "New York, NY",
  "city": "New York",
  "state": "NY",
  "latitude": 40.7128,
  "longitude": -74.006,
  "country": "us",
  "jobTypes": ["Full-time"],
  "workSettings": ["Hybrid"],
  "remoteAttributes": [],
  "salary": "$140,000 - $180,000 a year",
  "salaryMin": 140000,
  "salaryMax": 180000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "compensation": { "currency": "USD", "period": "year" },
  "snippet": "Short listing preview text appears here...",
  "descriptionHtml": "<p>Full long-form HTML description appears here.</p>",
  "descriptionText": "Full long-form description as plain text appears here.",
  "requirements": ["Python", "AWS"],
  "qualifications": ["Bachelor's degree", "5+ years experience"],
  "benefits": ["Health insurance", "401(k)"],
  "skillsAll": ["Python", "AWS", "API design"],
  "datePosted": "2026-09-20T08:00:00.000Z",
  "datePostedEpochMs": 1789891200000,
  "dateOnIndeed": 1789891200000,
  "applyUrl": "https://www.simplyhired.com/out?r=sampletoken000",
  "mobileApplyUrl": "https://www.simplyhired.com/m/out?r=sampletoken000",
  "indeedApply": false,
  "sponsored": false,
  "auction": false,
  "expired": false,
  "jobCardTrackingKey": "0-pdx0-0-0000000000000000-0000000000000000",
  "jobTypeRequested": "fulltime",
  "searchQuery": "software engineer",
  "searchLocation": "New York, NY",
  "pageNumber": 1,
  "source": "SimplyHired",
  "scrapedAt": "2026-09-29T12:00:00.000Z"
}
```

### Plan Requirement

SimplyHired sits behind a Cloudflare managed challenge ("Just a moment..."). In our measurements, plain requests, Apify datacenter proxy and Apify residential proxy (US) were all challenged on the search and home pages, so no single proxy group is guaranteed to get through. The prefilled proxy setting uses the Apify Residential group pinned to the country site you query, and the actor then escalates automatically through further connection routes, including a backup route, whenever a page comes back as a challenge or is refused. Results depend on the site's current defence, so a run can occasionally fail; it then ends as FAILED instead of returning a misleading empty result.

If your plan does not include residential proxy access (Apify free plans do not), the log says "The selected connection is unavailable to this run. Apify free plans do not include residential proxies." and the run continues on a backup connection where some pages may still be unavailable. Upgrade at apify.com/pricing or supply your own proxy URLs in the Proxy field.

### FAQ

#### How much does it cost?

Billing is pay-per-event, and the **Pricing** tab shows the current rates. The events are: `apify-actor-start` once per run; `apify-default-dataset-item` once per listing row pushed to the dataset; and `detail-enrichment` once per listing whose detail page was fetched successfully and whose row was delivered (it is not charged for rows that fail to load a detail payload, or that are suppressed such as `UNCHANGED` rows with Emit unchanged off). With **Fetch details** off, only the start and per-row events apply. Use **Max listings** to cap the cost of any run.

#### Is it legal to scrape SimplyHired?

This actor collects only publicly available job listing data. You are responsible for how you use it: follow SimplyHired's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

#### 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 updated listings, and unchanged ones are not billed unless you turn on **Emit unchanged**. Turn on **Emit expired** to also get EXPIRED listings after a run completes a full, uncapped scan; REAPPEARED appears only after such a run has marked a listing expired.

#### Why do my keyword and location filters have no effect?

Search-builder fields (`queries`, `location`, `jobType`) only apply when **mode** is `search`. In `url` mode, the scraper follows whatever filters are already baked into the pasted URL and ignores the builder fields entirely, since two different filter sources for the same run would conflict.

#### Why did my run return zero listings instead of failing?

If no results page could be loaded on any connection route, the run ends as FAILED with a status message instead of finishing green with an empty dataset, and you are not billed for a page that was never read. The message is one of: "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."; "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"; or, on a free plan, "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 SimplyHired pages." Try again in a few minutes or supply your own proxy URLs. A run that loaded pages but found nothing logs "No listings returned." and finishes normally. The run fails outright when it can detect a configuration problem immediately, such as an unreadable `resumeFromRunId`, turning on Incremental mode together with `resumeFromRunId` while a monitoring baseline already exists for that search, or the proxy configuration itself failing to set up.

#### 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/ziprecruiter-com-scraper"><b>ZipRecruiter Scraper</b></a><br>Scrape ZipRecruiter jobs with titles, companies, salaries, locations, benefits, apply...</td><td>💼 <a href="https://apify.com/abotapi/jobs-cz-scraper"><b>Jobs.cz</b></a><br>Scrape Czech job listings from Jobs.cz. Search builder (keyword, location, field, salary...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/jora-jobs-scraper"><b>Jora Jobs Scraper</b></a><br>Scrape Jora.com job listings across supported countries by keyword, filters, or URL...</td><td>💼 <a href="https://apify.com/abotapi/seek-scraper"><b>SEEK Jobs Scraper</b></a><br>Scrape SEEK.com.au and SEEK.co.nz jobs by keyword, location, or filters. Extract full...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/jobstreet-scraper"><b>JobStreet Scraper</b></a><br>Scrape JobStreet listings across Malaysia, Singapore, Indonesia, and the Philippines...</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/simplyhired-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 simplyhired.com /search or /job URL refined in your browser.

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

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

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

City, state, ZIP, or "Remote". Examples: "New York, NY", "Boston, MA", "94103", "Remote". Leave empty to search nationwide.

## `country` (type: `string`):

Which SimplyHired regional site to query. The site language follows the host. Defaults to the US site.

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

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

## `skipSponsored` (type: `boolean`):

Drop sponsored / promoted job cards. Applied as a client-side filter.

## `datePosted` (type: `string`):

Only jobs posted within this window. Sent to the site as a native filter in Search mode and also applied to each row's post date on this side.

## `minSalary` (type: `integer`):

Only jobs whose listed minimum pay is at least this yearly amount (hourly, weekly and monthly pay is annualised; jobs with no listed pay are dropped). Sent to the site as a native filter in Search mode and also checked on this side. 0 = off.

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

Keep only jobs flagged remote. Applied on this side to each listing.

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

SimplyHired ignores its own sort parameters, so 'Newest first' orders the listings of each fetched page by post date on this side; it does not re-order across pages.

## `keyword` (type: `string`):

Single-keyword alias for the Keywords field, accepted so runs built for other SimplyHired scrapers keep working. Ignored when Keywords is filled.

## `results_wanted` (type: `integer`):

Alias for Max listings. Used when Max listings is left unset or still at its default of 20; a Max listings value you changed yourself takes priority.

## `max_pages` (type: `integer`):

Alias for Max pages per search, used only when that field is empty.

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

One or more simplyhired.com URLs (also the .co.uk, .ca, .com.au, .ie and .co.in sites). Supported: keyword + location search, keyword-only, location-only, paged (pn), job-type filtered (jt), search with a job= preselect, and a single /job page. Each search URL pages forward independently. Only the keyword, location, job type and start page come from the pasted URL; the Date posted, Minimum yearly salary, Remote only, Sort by and Skip sponsored fields above are still applied on this side to the results of every URL, so clear them if you want the pasted search unfiltered.

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

Alias for the Search URLs field. When filled it replaces the sample Search URLs, and when no keywords are given the run switches to URL mode automatically. A Search URLs list you edited yourself takes priority.

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

How many SERP pages to walk per query × location combination (or per pasted URL). Each page returns up to 20 listings. Leave empty to walk every result page. Use `maxListings` in tandem to cap the total budget across all searches.

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

Stop after this many listings across all searches. 0 = no cap.

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

When ON, also fetch each job's detail page for the full HTML description, qualifications list, employer logo, lat/lng, and apply URL. SERP cards already carry id, title, company, location, snippet, and posted date, so simple feeds work fine with this OFF. Default ON for richer output.

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

SimplyHired requires residential routing pinned to the country you are querying (Datacenter exit IPs are rejected by the upstream edge). The default below works on Apify Starter and higher. Free plans without residential access can paste their own residential proxy URLs in this field.

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

Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. Listings already collected there (matched by id) are skipped, so this run only saves the new ones. Leave empty for a normal fresh run. 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. A listing counts as UPDATED when any returned field other than the ignored ones changes (title, company, location, salary, job type, description, qualifications, benefits and so on); ignored by default and never compared: scrapedAt, applyUrl, mobileApplyUrl, jobCardTrackingKey, pageNumber and auction (they change with every scrape), plus jobKey, summary, skills, jobType and sourceSearchUrl (copies or derivations of compared fields: id, snippet or descriptionText, skillsAll, jobTypes and your search inputs, so a real change still shows through the source field). A plain field name is ignored at any depth, including inside nested objects (see Ignore fields when detecting changes to add more). Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately for each query/location/URL and detail-mode setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous 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 query/location/URL and detail 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 or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Optional. Field names that must not make a listing count as UPDATED, in addition to the built-in ignored fields. A plain name (for example employerLogoUrl) is ignored at any depth; a dotted path (for example compensation.min) ignores exactly that nested field. The fields are still returned in the output, just never compared. Leave empty to use the defaults.

## `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": [
    "software engineer"
  ],
  "location": "Remote",
  "country": "us",
  "jobType": "any",
  "skipSponsored": false,
  "datePosted": "any",
  "minSalary": 0,
  "remoteOnly": false,
  "sortBy": "relevance",
  "urls": [
    "https://www.simplyhired.com/search?q=software+engineer&l=Remote",
    "https://www.simplyhired.com/search?q=nurse&l=Boston%2C+MA&pn=2",
    "https://www.simplyhired.com/search?q=engineer&l=Remote&jt=CF3CP",
    "https://www.simplyhired.com/search?q=nurse",
    "https://www.simplyhired.com/search?l=Remote",
    "https://www.simplyhired.com/search?q=engineer&l=Remote&job=zzz",
    "https://www.simplyhired.com/job/mo_LRS0yJ90r4G7Bi9HsIQfPAspEk3xIq65MtLP0wZaC8PKRelCDBQ"
  ],
  "maxListings": 20,
  "fetchDetails": true,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "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": [
        "software engineer"
    ],
    "location": "Remote",
    "urls": [
        "https://www.simplyhired.com/search?q=software+engineer&l=Remote",
        "https://www.simplyhired.com/search?q=nurse&l=Boston%2C+MA&pn=2",
        "https://www.simplyhired.com/search?q=engineer&l=Remote&jt=CF3CP",
        "https://www.simplyhired.com/search?q=nurse",
        "https://www.simplyhired.com/search?l=Remote",
        "https://www.simplyhired.com/search?q=engineer&l=Remote&job=zzz",
        "https://www.simplyhired.com/job/mo_LRS0yJ90r4G7Bi9HsIQfPAspEk3xIq65MtLP0wZaC8PKRelCDBQ"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/simplyhired-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": ["software engineer"],
    "location": "Remote",
    "urls": [
        "https://www.simplyhired.com/search?q=software+engineer&l=Remote",
        "https://www.simplyhired.com/search?q=nurse&l=Boston%2C+MA&pn=2",
        "https://www.simplyhired.com/search?q=engineer&l=Remote&jt=CF3CP",
        "https://www.simplyhired.com/search?q=nurse",
        "https://www.simplyhired.com/search?l=Remote",
        "https://www.simplyhired.com/search?q=engineer&l=Remote&job=zzz",
        "https://www.simplyhired.com/job/mo_LRS0yJ90r4G7Bi9HsIQfPAspEk3xIq65MtLP0wZaC8PKRelCDBQ",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/simplyhired-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": [
    "software engineer"
  ],
  "location": "Remote",
  "urls": [
    "https://www.simplyhired.com/search?q=software+engineer&l=Remote",
    "https://www.simplyhired.com/search?q=nurse&l=Boston%2C+MA&pn=2",
    "https://www.simplyhired.com/search?q=engineer&l=Remote&jt=CF3CP",
    "https://www.simplyhired.com/search?q=nurse",
    "https://www.simplyhired.com/search?l=Remote",
    "https://www.simplyhired.com/search?q=engineer&l=Remote&job=zzz",
    "https://www.simplyhired.com/job/mo_LRS0yJ90r4G7Bi9HsIQfPAspEk3xIq65MtLP0wZaC8PKRelCDBQ"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/simplyhired-com-scraper --silent --output-dataset

```

## MCP server setup

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