# SEEK Scraper (`mlg14/seek-scraper`) Actor

Scrape Australia and New Zealand job listings by keyword, location, or URL. Export titles, employers, salary, dates, work arrangements, and optional full descriptions.

- **URL**: https://apify.com/mlg14/seek-scraper.md
- **Developed by:** [MLG Data](https://apify.com/mlg14) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.60 / 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.

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 Scraper

Scrape SEEK jobs across Australia and New Zealand by keywords, location, or a direct search URL. Export SEEK job listings to CSV or JSON with salary text, work arrangements, posting dates, employer details, and optional full descriptions. This scraper offers a repeatable SEEK API alternative for collecting public job advertisements without manually paging through search results.

Each dataset row represents one distinct job ID. A run can combine several keyword and location searches, add pasted search URLs, and stop at a chosen result limit. Full details add the complete description, structured location, and verification status.

### What data can you extract from SEEK?

Every output field has a stable name. Optional values are `null` when a job advertisement does not expose them. Salary, location, and advertiser detail coverage depends on what the advertiser publishes. Array fields are empty arrays when there are no points to report.

| Field | Description | Example |
| --- | --- | --- |
| `jobId` | Listing ID used for deduplication | `94483533` |
| `seekJobId` | Listing ID as displayed by the site | `94483533` |
| `title` | Advertised job title | `Graduate Software Engineer` |
| `canonicalUrl` | Direct link to the advertisement | `https://au.seek.com/job/94483533` |
| `company` | Advertiser name, when published | Text or `null` |
| `advertiserId` | Public advertiser identifier | Numeric string |
| `companyUrl` | Advertiser profile URL, if present | URL or `null` |
| `location` | Location displayed on the listing | `Macquarie Park, Sydney NSW` |
| `locationCountry` | Job market | `AU` or `NZ` |
| `locationState` | Structured state or region from the detail page | `New South Wales` |
| `locationSuburb` | Structured suburb from the detail page | `Macquarie Park` |
| `locationPostcode` | Structured postal code | `2113` |
| `salaryText` | Pay wording shown in the advertisement | `$95,000 – $110,000 per year` |
| `salaryMin` | Parsed lower amount if an amount is shown | `95000` |
| `salaryMax` | Parsed upper amount if a range is shown | `110000` |
| `salaryCurrency` | Currency inferred from the market | `AUD` or `NZD` |
| `salaryType` | Parsed pay period when stated | `year`, `day`, or `hour` |
| `employmentType` | Advertised work type | `Full time` |
| `workArrangement` | Workplace arrangement | `On-site`, `Hybrid`, or `Remote` |
| `category` | Broad job classification | `Information & Communication Technology` |
| `subCategory` | Narrow classification | `Engineering - Software` |
| `roleId` | Role label used by the site | `software-engineer` |
| `postedDate` | Listing timestamp | ISO timestamp |
| `validThrough` | Expiration timestamp if exposed | ISO timestamp or `null` |
| `teaser` | Brief ad summary | Short text |
| `bulletPoints` | Highlight points from search results | String array |
| `description` | Full description as readable text | Text or `null` |
| `descriptionHtml` | Original formatted job description | HTML or `null` |
| `descriptionMarkdown` | Plain Markdown description | Text or `null` |
| `screeningQuestions` | Public screening questions | String array |
| `phoneNumber` | Phone published in the ad | Text or `null` |
| `applyEmail` | Application email when exposed | Text or `null` |
| `contactName` | Public contact name when exposed | Text or `null` |
| `extractedEmails` | Emails in the public description | String array |
| `extractedPhones` | Phones in the public description | String array |
| `extractedUrls` | Links in the public description | String array |
| `socialProfiles` | Public social links when exposed | String array |
| `companyLogo` | Logo image URL when present | URL or `null` |
| `companyCoverImageUrl` | Cover image URL when present | URL or `null` |
| `companyDescription` | Public advertiser profile text when exposed | Text or `null` |
| `companyIndustry` | Advertiser industry when exposed | Text or `null` |
| `companySize` | Advertiser size when exposed | Text or `null` |
| `companyWebsite` | Advertiser website when exposed | URL or `null` |
| `companyPerks` | Public advertiser perks | String array |
| `companyReviewRating` | Public advertiser rating when exposed | Number or `null` |
| `companyReviewCount` | Public advertiser review count | Number or `null` |
| `companySearchUrl` | Search page for jobs from the advertiser | URL or `null` |
| `isVerifiedAdvertiser` | Verification flag on the job page | Boolean or `null` |
| `applyUrl` | External application link if exposed | URL or `null` |
| `isExpired` | Expiration status from the detail page | Boolean or `null` |
| `isFeatured` | Featured flag from the search result | Boolean or `null` |
| `applicantCount` | Applicant count when public | Number or `null` |
| `applicantVolumeLabel` | Applicant volume label when public | Text or `null` |
| `coverLetterPercentage` | Cover-letter share when public | Number or `null` |
| `searchQuery` | Keyword source that found the row | `software engineer` |
| `scrapedAt` | Extraction time | ISO timestamp |

The search card may show a salary label without a clean amount. Then `salaryText` retains the wording and numeric fields remain `null`. Hourly pay is not converted to annual pay.

### How to scrape SEEK

1. Enter job keywords in `query`, choose `AU` or `NZ`, and add a location if you want to focus on a city or region. For a broader search, leave location empty.
2. Set `maxResults` to the number of distinct jobs you need. Set `maxPages` to control how far each search source can paginate. A pasted search URL can be added in `startUrls`.
3. Keep `includeDetails` enabled when you need the full ad text or structured location. Turn it off for a faster search-card dataset.
4. Run the scraper. The dataset fills as listings are found and stops at the result cap.
5. Download the dataset as JSON or CSV, or access it through the dataset API for scheduled processing.

For repeat searches, keep inputs consistent and compare `jobId` across runs. Overlapping sources emit each ID once per run.

### Input

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `query` | String | Empty | Keywords; a JSON-encoded list runs multiple keyword searches. |
| `country` | String | `AU` | `AU` for Australia or `NZ` for New Zealand. |
| `location` | String | Empty | City, state, or region; a JSON-encoded list runs several location searches. |
| `startUrls` | String array | Empty | Direct search or individual job URLs from either market. |
| `maxResults` | Integer | `50` | Total distinct jobs to output; `0` removes this cap. |
| `maxPages` | Integer | `25` | Maximum pages visited for each search. |
| `sortMode` | String | `relevance` | Relevance or newest listed first. |
| `dateRange` | String | Empty | Posting window: 1, 3, 7, 14, or 31 days. |
| `workType` | String | Empty | Optional site work type code. |
| `workArrangement` | String | Empty | Optional site workplace arrangement code. |
| `classification` | String | Empty | Optional broad classification code. |
| `subClassification` | String | Empty | Optional narrow classification code. |
| `includeDetails` | Boolean | `true` | Add the full job page data. |
| `includeKeywords` | String array | Empty | Keep jobs containing at least one listed term. |
| `excludeKeywords` | String array | Empty | Drop jobs containing any listed term. |
| `proxyConfiguration` | Object | Automatic | Override the proxy settings when needed. |

A typical search for current jobs in one city is:

```json
{
  "query": "software engineer",
  "country": "AU",
  "location": "Sydney",
  "sortMode": "date",
  "maxResults": 35,
  "maxPages": 3,
  "includeDetails": true
}
```

To search several phrases in one run, put a JSON list inside the string field: `"[\"data analyst\", \"business analyst\"]"`. The same pattern works for locations. Each keyword and location combination becomes a separate search source. Jobs are merged by listing ID, so overlapping combinations do not duplicate rows. Direct URLs add further sources; they do not replace keyword searches.

`startUrls` accepts search pages such as `https://au.seek.com/jobs?keywords=accountant` and individual job pages such as `https://nz.seek.com/job/94818442`. A direct job URL returns a row only when its public page has a readable title and is not marked expired. A search URL retains its own path and query filters while the scraper advances its page number. A bare homepage is not a search source.

For filtering after collection, `includeKeywords` checks title, advertiser, location, teaser, and full description. `excludeKeywords` checks title, advertiser, teaser, and full description. If a filter depends on description text, leave `includeDetails` on. The result cap applies to rows emitted after filtering, so a narrow filter may cause the scraper to visit more listings than the final count suggests.

### Output example

This trimmed record came from a successful Australia search with details enabled. The complete dataset row also contains the remaining fields listed above. The original description and image URL are omitted here to keep the example readable.

```json
{
  "jobId": "94483533",
  "seekJobId": "94483533",
  "title": "Graduate Software Engineer",
  "canonicalUrl": "https://au.seek.com/job/94483533",
  "location": "Macquarie Park, Sydney NSW",
  "locationCountry": "AU",
  "locationState": "New South Wales",
  "locationSuburb": "Macquarie Park",
  "locationPostcode": "2113",
  "salaryText": null,
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": "AUD",
  "employmentType": "Full time",
  "workArrangement": "On-site",
  "category": "Information & Communication Technology",
  "subCategory": "Engineering - Software",
  "postedDate": "2026-09-08T03:09:03.858Z",
  "isVerifiedAdvertiser": true
}
```

Import `jobId` as text in spreadsheets. Timestamps use UTC. A `null` optional field means unavailable, not a negative finding; `salaryText: null` does not imply unpaid work.

### Use cases

- **Recruiting research:** Review public openings by role, place, work arrangement, and posting date to prioritize a market segment.
- **Salary benchmarking:** Collect disclosed pay labels with the original text and parsed numeric amounts, then compare like pay periods and currencies.
- **Regional hiring trends:** Compare AU and NZ job volumes, classifications, and newly listed roles across repeat runs.
- **Career discovery:** Build a filtered feed for several keywords and nearby locations without revisiting overlapping listings.
- **Employer monitoring:** Track the public job IDs and posting dates associated with a set of targeted searches.
- **Job-board analysis:** Measure how often advertisers publish location, salary, and remote-work information in selected categories.

These uses rely on public advertisements. Job availability and pay wording can change between visits. For monitoring, retain collection times and compare IDs as well as posting dates.

### How much does it cost to scrape SEEK?

The configured result charge is **$1.60 per 1,000 emitted jobs**, or **$0.0016 per output row**. This charge applies to rows delivered to the dataset. Platform resource usage and proxy traffic may add cost, especially when full details require a page visit for each job. The amount paid for a run therefore depends on both the emitted count and the traffic needed to obtain it.

| Emitted jobs | Result charge calculation | Result charge |
| ---: | ---: | ---: |
| 100 | `100 × $0.0016` | `$0.16` |
| 1,000 | `1,000 × $0.0016` | `$1.60` |
| 5,000 | `5,000 × $0.0016` | `$8.00` |

Start with 30–50 results to assess field coverage. Disable `includeDetails` when only search-card columns are needed. A low limit cannot guarantee that many results if a search returns fewer distinct jobs.

### Tips for best results

Use a specific keyword and a location when you want a manageable, relevant dataset. A broad term in a large metro area may span many result pages. For wider coverage, run several role or location slices rather than expecting one very broad search to expose every matching advertisement. The scraper deduplicates IDs within each run but does not preserve a history between runs; compare exported datasets for longitudinal tracking.

Date sorting is useful for frequent monitoring. Keep a consistent `dateRange` and store the `scrapedAt` value so you can separate jobs newly posted since the previous run from old jobs that moved in search rankings. Relevance sorting may include promoted jobs at the top; it should not be interpreted as strict posting order.

Prefer full details when you need the description, structured suburb, state, postal code, verification flag, or the job page's more complete salary wording. The search-card mode is faster and adequate for titles, employers, locations, dates, categories, and teasers. Some cards already contain work arrangement and salary data, but detail pages can refine them. The `maxPages` limit applies separately to each query, location combination, or pasted search URL.

For NZ searches, the scraper reads structured data embedded in the public search page. For AU keyword searches, it uses the public JSON search response. Both are public sources and both can change as the site changes. Direct URLs are useful for matching an exact search configured in the site's interface, particularly when it has filters you prefer to set there first.

### Limits

The scraper only returns public job pages. It does not sign in, apply, or access private applicant statistics. Broad search counts may exceed reachable pages. A high `maxPages` value cannot overcome a search result cap. Split broad searches by location, keyword, or classification when you need broader coverage, and deduplicate across runs by `jobId`.

Search pages may include promoted jobs or repeated IDs. The actor emits each ID once. An advertisement can disappear or expire between the search request and the detail request; in that case the search-card fields may remain while detail fields are empty. Some employers do not provide pay ranges, logos, profile descriptions, phone numbers, or external application links. Those fields stay `null` rather than receiving guessed values.

Applicant counts, screening questions, and application contact details are not exposed by the current public source and remain empty. Public pages have access controls. Datacenter requests were blocked during verification, while residential requests returned structured data. The fetch layer rotates its session and escalates its proxy when blocked. This can increase runtime and traffic cost. If access controls change, a run may yield fewer rows or fail until the extraction path is adjusted. Very broad searches can also be slower because full details add one request for each distinct job considered.

Pay parsing is conservative. It extracts visible numbers and recognizes common hourly, daily, and yearly wording. It does not normalize currencies, estimate undisclosed compensation, or infer whether an amount is base salary plus benefits. For compensation analysis, retain `salaryText` alongside the parsed amounts and filter to comparable `salaryType` values. The market sets `salaryCurrency`; it is not an exchange conversion.

### Automated workflows

Schedule the same input, then compare `jobId` with prior datasets. Filters can route selected roles into a review queue. Treat optional fields as nullable.

For example: “Collect up to 50 software engineer jobs in Sydney, including descriptions, and keep roles mentioning a selected skill.” Or: “Collect New Zealand analyst jobs in Auckland, sorted by listing date, and show title, location, work arrangement, and posting date.” The actor's input can be reused in a schedule or an API call, and its dataset URL can feed the next processing step.

### FAQ

#### Is it legal to collect these listings?

The scraper collects publicly visible job advertisements. You are responsible for respecting the site's terms, applicable privacy law, and any limits on reuse. Avoid personal-data misuse and collect only the fields your workflow needs. Public availability does not by itself grant unlimited republication rights.

#### Do I need to configure proxies?

The default proxy settings are intended for normal runs. Public pages blocked datacenter traffic during verification, so the fetch layer may escalate to a residential session. You can provide your own proxy configuration if your account and workload require it. Changes to site access controls may still affect results.

#### How fast is a run?

Search-card runs need relatively few requests because each page carries multiple jobs. Full-detail runs fetch a job page for every distinct listing that reaches enrichment, so they take longer. Runtime also varies with the number of search sources, filtering, page depth, and access-control retries. A verified 35-job AU run with details completed successfully; treat that as a functional check, not a fixed speed guarantee.

#### Can I schedule a recurring search?

Yes. Reuse the same input on a schedule, export each dataset, and compare `jobId` across runs. A posting date helps order the results, while `scrapedAt` records when the row was collected. This actor does not maintain a built-in change history between runs.

#### Can I export to a spreadsheet?

Yes. Download CSV for a spreadsheet or JSON for a programmatic pipeline. Import the ID as text to preserve it exactly. The overview table surfaces common columns, while the downloaded row includes the complete field set.

#### Why is salary or advertiser information empty?

Advertisers choose what to publish. Salary, profile, logo, phone, and some structured location fields are not universal. Detail enrichment can add fields missing from the search card, but it cannot create values that the public page does not expose. An empty value is represented as `null` or an empty array.

#### Can I scrape an individual job URL?

Yes. Put a supported AU or NZ `/job/<id>` URL in `startUrls`. The scraper reads its public structured detail data and emits a row if the job is readable and not marked expired. Search pages can be supplied in the same input and will paginate until the relevant cap is reached.

### Integrations

Use the dataset API or download a file to move rows into a database, spreadsheet, dashboard, or notification workflow. Webhooks and scheduled runs can hand the dataset URL to downstream services. Keep the exported schema stable by reading field names rather than table column positions, and account for `null` in optional fields. For monitoring, store prior IDs and timestamps outside the actor so each run can be compared with earlier collections.

### Support

Open an issue on the Issues tab; we reply within 24h and add fields on request.

# Actor input Schema

## `query` (type: `string`):

Search words. A JSON-encoded list runs several searches and merges jobs by ID.

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

Choose the Australia or New Zealand job market.

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

City, state or region. A JSON-encoded list searches several locations.

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

Direct AU or NZ search pages and individual job URLs. Added to keyword searches.

## `maxResults` (type: `integer`):

Maximum jobs across every search. Zero removes this limit.

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

Maximum result pages fetched for each search source.

## `sortMode` (type: `string`):

Sort by relevance or listing date.

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

Restrict results to a recent posting window.

## `workType` (type: `string`):

Optional work type code: 242 full time, 243 part time, 244 contract, 245 casual.

## `workArrangement` (type: `string`):

Optional arrangement code: 1 on-site, 2 hybrid, 3 remote.

## `classification` (type: `string`):

Optional job family classification code from a search URL.

## `subClassification` (type: `string`):

Optional sub-classification code from a search URL.

## `includeDetails` (type: `boolean`):

Fetch each job page for full description, structured location, and advertiser details.

## `includeKeywords` (type: `array`):

Keep jobs containing at least one of these words in title, company, location, teaser or description.

## `excludeKeywords` (type: `array`):

Drop jobs containing any of these words in title, company, teaser or description.

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

Proxy settings. A residential proxy is used after public pages block datacenter access.

## Actor input object example

```json
{
  "query": "software engineer",
  "country": "AU",
  "location": "Sydney",
  "maxResults": 50,
  "maxPages": 25,
  "sortMode": "relevance",
  "dateRange": "",
  "includeDetails": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Structured jobs in the default dataset.

# 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 = {
    "query": "software engineer",
    "location": "Sydney",
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("mlg14/seek-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 = {
    "query": "software engineer",
    "location": "Sydney",
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("mlg14/seek-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 '{
  "query": "software engineer",
  "location": "Sydney",
  "maxResults": 50
}' |
apify call mlg14/seek-scraper --silent --output-dataset

```

## MCP server setup

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