# SEEK Jobs Scraper - New Jobs & Salary Change Monitor (`neverempty/seek-new-jobs-monitor`) Actor

For recruiters, staffing agencies and job alerts: SEEK jobs in Australia and New Zealand, newest first, with company, location, work type and listing date; salary as shown, plus amounts and currency when SEEK gives them. Schedule it to get only new jobs, salary changes and removed jobs.

- **URL**: https://apify.com/neverempty/seek-new-jobs-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.90 / 1,000 job returneds

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

## SEEK Jobs Scraper - New Jobs & Salary Change Monitor (AU, NZ)

**Schedule it hourly or daily and get only new SEEK jobs, salary changes and removed jobs.** For recruiters, staffing agencies, job alert bots and anyone tracking hiring in Australia and New Zealand: the newest jobs SEEK lists for your search, with the company, location, category, work type, the salary as SEEK shows it (plus the amounts and currency when the text has them), the listing date and the job link. Turn on monitor mode and each scheduled run returns only what moved since the last run, with the previous salary in the same row, so you never pay twice for the same job.

Start with just a keyword. Export as JSON, CSV or Excel, or send the new jobs to Slack, email or a sheet with an Apify integration.

### What you get

- **New jobs only, on a schedule** (`monitorMode`). Each row says what happened in `changeType`: `new` (listed since the last run), `changed` (the salary text changed; the previous salary is in `previousSalaryLabel`, `previousSalaryMin`, `previousSalaryMax`, and `salaryDirection` says `up` or `down`), or `removed` (a job you received is no longer in the newest-first list on two checks in a row: filled or taken down). The first run returns the current jobs once as the starting point.
- **Newest first, up to 2,000 jobs per search.** Big searches are read in full where SEEK allows it; a free row tells you when SEEK limits one.
- **Salary you can sort.** `salaryLabel` keeps SEEK's own text (`$40 – $53 per hour`, `$120k - $140k + super`, `Competitive salary`). `salaryMin`, `salaryMax` and `salaryPeriod` (`hour`, `day`, `week`, `fortnight`, `month`, `year`) are read from it, with `salaryCurrency` (`AUD` or `NZD`). When the text has no amount, or the job shows no salary, these stay empty; nothing is guessed.
- **The filters SEEK has:** keywords, location (suburb, city, region, state or postcode), country (Australia or New Zealand), category, work type (full time, part time, contract/temp, casual/vacation), salary range (annual, monthly or hourly) and listing date range. Or paste any SEEK search results URL with your filters already set.
- **Each job once.** A job found by two searches of the same run is returned and charged once. In monitor mode, a job you already received is never sold as `new` again, even when it shows up under another search of the same watch.
- **Jobs already returned are never sent as new again**, and older jobs that only moved up the list are not sold as new.
- **Nothing found is not a failure.** No jobs, no changes, an unknown location or a spending limit come back as a free row that says why. If SEEK refuses every request, the run ends as failed and says so.

### Output

One row per job. Main columns:

| Column | Meaning |
|---|---|
| `status` | `ok` for a job row. Other values are free rows (below) |
| `changeType` | Monitor mode: `new`, `changed`, `removed` or `unchanged` (only with `includeUnchanged`). Empty in a normal search. `removed` rows carry the title, company, last salary, listing date and link only |
| `isFirstCheck` | `true` on the first run of a watch, when the current jobs are returned as the starting point |
| `id`, `url` | SEEK job ID and the job page |
| `title` | Job title |
| `companyName`, `companyId`, `advertiserId`, `companyProfileUrl`, `logoUrl` | The employer as shown on SEEK |
| `location`, `area`, `locationHierarchy`, `countryCode` | Where the job is, as SEEK lists it |
| `classification`, `subclassification` (and their IDs) | SEEK category and sub-category |
| `workTypes`, `workArrangements` | Full time, part time, contract/temp, casual/vacation; on-site, hybrid or remote when SEEK shows it |
| `salaryLabel`, `salaryMin`, `salaryMax`, `salaryPeriod`, `salaryCurrency` | Salary text as shown, and the amounts read from it |
| `previousSalaryLabel`, `previousSalaryMin`, `previousSalaryMax`, `previousSalaryPeriod`, `salaryDirection` | Monitor mode, `changed` rows: the salary before the change, and `up` or `down` when both are amounts in the same period |
| `teaser`, `bulletPoints` | The short description and the bullet points shown in the search results |
| `listedAt`, `listedAgo` | Listing date (UTC) and SEEK's "2h ago" text |
| `isFeatured`, `adType`, `tags` | Whether SEEK shows the job as a promoted listing, its listing type, and the labels SEEK puts on it |
| `positionInSearch`, `jobsInSearch` | Position in the newest-first list, and the number of jobs SEEK reports for the search |
| `searchKeywords`, `searchLocation`, `searchLocationResolved`, `searchCountry`, `searchUrl` | The search the row came from; `searchUrl` opens the same search on SEEK |
| `watchName`, `checkedAt` | The watch name you gave, and the time of the check |
| `note` | Free rows only: what happened and what to do |

#### Free rows (never charged)

| `status` | When |
|---|---|
| `no-results` | SEEK lists no job for this search. |
| `no-changes` | Monitor mode: no new jobs, salary changes or removed jobs since the last run. |
| `none-matching` | SEEK has jobs, but each one was already returned by an earlier search of the same run or watch. |
| `more-not-returned` | A normal search has more jobs than SEEK lets one search reach even after splitting it; the row says how many and how to narrow it. Monitor mode: more changes than `maxResultsPerSearch` (a later run returns them), or on the first run, how many jobs became the starting point. |
| `more-new-than-readable` | Monitor mode: more jobs were listed since the last run than this run read, so some new ones may have been missed. Run the watch more often or raise `maxResultsPerSearch`. |
| `not-found` | SEEK could not use the search, or did not recognise the location. |
| `blocked` | SEEK refused the request. The run stops and says which searches were not run. |
| `unreadable` | A page could not be read, even after asking again. Nothing is guessed, and in monitor mode nothing is remembered: the next run checks it again. |
| `budget-reached` | The run reached the maximum total charge you set; the rest was not returned and not charged (in monitor mode the next run returns them). |
| `bad-input` | The input could not be used (for example a date range SEEK does not offer). Nothing was requested. |

### Input

| Field | What it does |
|---|---|
| `keywords` | What you would type into SEEK: `nurse`, `data analyst`, `electrician`. Each keyword is searched in each location. All search fields empty = nurse jobs in all of Australia. |
| `country` | `AU` (seek.com.au) or `NZ` (seek.co.nz). Empty = Australia. |
| `locations` | Suburbs, cities, regions, states or postcodes as you would type them on SEEK: `Sydney NSW`, `Melbourne`, `4000`, `Auckland`. Empty = the whole country. |
| `classifications` | SEEK categories, for example Healthcare & Medical or Information & Communication Technology. Empty = all. |
| `workTypes` | Full time, part time, contract/temp, casual/vacation. Empty = all. |
| `salaryMin`, `salaryMax`, `salaryType` | Pay range in AUD or NZD, per year, month or hour, applied by SEEK the same way as its own salary filter. |
| `dateRange` | Only jobs listed within 1, 3, 7, 14 or 31 days. |
| `searchUrls` | SEEK search results pages copied from the browser, for example `https://www.seek.com.au/nurse-jobs/in-Sydney-NSW`. Each is read newest first with its own filters. |
| `maxResultsPerSearch` | Newest jobs to read per search, 1-2000. Empty = 100. In monitor mode, the window checked for new jobs and changes on each run. |
| `monitorMode` | Return only jobs that are new since the last run, salary changes and removed jobs. |
| `includeChanged`, `includeRemoved` | Monitor mode: return salary changes and removed jobs. Empty = yes. |
| `includeUnchanged` | Monitor mode: also return jobs already received that did not change (each one is charged). Empty = no. |
| `watchName` | Give each schedule its own name so separate schedules keep separate memories. |
| `resetMonitoringState` | Forget what this watch remembered; this run is a first check again. |

Up to 10 searches per run (keywords times locations, plus search URLs).

### Examples

New nursing jobs in Sydney and Melbourne, every hour:

```json
{ "keywords": ["registered nurse"], "locations": ["Sydney NSW", "Melbourne VIC"], "monitorMode": true, "watchName": "rn-syd-mel" }
```

Full-time IT jobs in Auckland paying at least NZD 100,000, listed in the last 3 days:

```json
{ "country": "NZ", "locations": ["Auckland"], "classifications": ["information-communication-technology"], "workTypes": ["full-time"], "salaryMin": 100000, "dateRange": "3" }
```

The newest 1,000 jobs across all of Australia for a keyword:

```json
{ "keywords": ["electrician"], "maxResultsPerSearch": 1000 }
```

### Monitor mode, step by step

1. First run: reads the newest `maxResultsPerSearch` jobs of each search, returns them once (`changeType: new`, `isFirstCheck: true`) and remembers them as the starting point.
2. Later runs: read the same window again and return only jobs listed since the last run (`new`), jobs you received whose salary text changed (`changed`, with the previous salary), and jobs you received that are gone from the list two runs in a row (`removed`). When nothing moved, one free `no-changes` row comes back and only the run start fee is charged.
3. Jobs not returned because of `maxResultsPerSearch` or a spending limit come back first in the next run. Nothing is judged from a page that could not be read.

Keep one schedule per input and `watchName`. Two overlapping runs of the same watch can both return the same new job.

### Pricing

You pay per job row returned, plus a small start fee once per run. In monitor mode the start fee pays for the check, also when nothing changed. Free rows are never charged, and unchanged jobs are not returned or charged unless you turn on `includeUnchanged`.

The first run of a watch returns and charges up to `maxResultsPerSearch` jobs per search (default 100) as the starting point; set it lower for a cheap first run.

### FAQ

**How often should I schedule it?** Hourly or daily. A busy search (for example a keyword across all of Australia) can get more new jobs in a day than a 100-job window holds; run it more often or raise `maxResultsPerSearch`, and a free row tells you when that happened.

**What counts as a salary change?** Any change of the salary text SEEK shows, including a salary added to a job that had none. A salary that disappears is not reported as a change.

**How many jobs does one search return?** Up to 2,000. SEEK pages one search through only its newest 500, so a bigger search is split by work type or category and merged back in date order, without gaps. A search that cannot be split (for example one narrowed to one work type and one category) returns its newest 500, and a split search whose parts are still too big returns the newest jobs SEEK lets it reach; either way a free row says how many more SEEK has. For more, run several searches (locations, categories or work types) in the same run; a job found twice is returned once.

**Does it work for New Zealand?** Yes. Set `country` to `NZ` or paste a seek.co.nz search URL. Salaries are in NZD.

### Good to know

- Works without a SEEK account or API key, and reads only what SEEK shows anyone without logging in.
- Unofficial and not affiliated with SEEK.
- Contact details of recruiters are not returned; the company is.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `keywords` (type: `array`):

Schedule it hourly or daily and get only new jobs, salary changes and removed jobs. Type what you would search on SEEK, for example nurse, data analyst, electrician; each keyword is searched in each location. All search fields empty = nurse jobs in all of Australia.

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

Which SEEK site to search. Empty = Australia. Pasted search URLs keep their own country.

## `locations` (type: `array`):

Suburbs, cities, regions, states or postcodes as you would type them on SEEK, for example Sydney NSW, Melbourne, 4000, Auckland. Empty = the whole country.

## `classifications` (type: `array`):

Optional. Only jobs in these SEEK categories, for example Healthcare & Medical or Information & Communication Technology. Empty = all categories.

## `workTypes` (type: `array`):

Optional. Only full time, part time, contract/temp or casual jobs. Empty = all work types.

## `salaryMin` (type: `integer`):

Optional. Lowest pay, in AUD for Australia or NZD for New Zealand, per the salary type below (annual by default). SEEK applies it the same way as its own salary filter.

## `salaryMax` (type: `integer`):

Optional. Highest pay, in AUD or NZD, per the salary type below (annual by default).

## `salaryType` (type: `string`):

Whether the minimum and maximum salary above are per year, per month or per hour. Empty = annual.

## `dateRange` (type: `string`):

Optional. Only jobs listed within this many days. Empty = any date.

## `searchUrls` (type: `array`):

Optional. Paste the address of any SEEK search results page with your filters already set, for example https://www.seek.com.au/nurse-jobs/in-Sydney-NSW. Each one is watched as its own search, always newest first.

## `maxResultsPerSearch` (type: `integer`):

How many of the newest jobs to read per search (1 to 2000). Empty = 100. In monitor mode this is the window checked for new jobs and changes on each run.

## `monitorMode` (type: `boolean`):

Schedule it hourly or daily and get only jobs that are new since the last run, jobs whose salary changed (with the previous salary) and jobs that were taken down. The first run returns the current jobs as the starting point.

## `includeChanged` (type: `boolean`):

In monitor mode, return jobs you already received whose salary text changed, with the previous salary. Empty = yes.

## `includeRemoved` (type: `boolean`):

In monitor mode, return jobs you already received that are no longer listed on two checks in a row (filled or taken down). Empty = yes.

## `includeUnchanged` (type: `boolean`):

In monitor mode, also return jobs already received that did not change (each one is charged). Empty = no.

## `watchName` (type: `string`):

Optional. Give each schedule its own name (letters, digits, dot, dash, underscore) so separate schedules keep separate memories. Empty = default.

## `resetMonitoringState` (type: `boolean`):

Forget what this watch remembered, so this run is a first check again.

## Actor input object example

```json
{
  "keywords": [
    "nurse"
  ],
  "maxResultsPerSearch": 100
}
```

# Actor output Schema

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

One row per SEEK job: ID, title, company, location, category, work type, salary text with amounts and currency, summary, key points, listed date and link. Monitor mode marks each row new, changed (with the previous salary), removed or unchanged. A search with no jobs, no changes, an unknown location or a refused request comes back as a free row that says why.

# 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 = {
    "keywords": [
        "nurse"
    ],
    "maxResultsPerSearch": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/seek-new-jobs-monitor").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 = {
    "keywords": ["nurse"],
    "maxResultsPerSearch": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/seek-new-jobs-monitor").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 '{
  "keywords": [
    "nurse"
  ],
  "maxResultsPerSearch": 100
}' |
apify call neverempty/seek-new-jobs-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/seek-new-jobs-monitor"
        }
    }
}
```

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/NgE0LCGaZZC6LHSxa/builds/yCdsu6NpK7D2bSPX9/openapi.json
