# JobStreet / JobsDB / SEEK Jobs Scraper (`scrapyx/jobstreet-jobs-scraper`) Actor

Scrapes job listings with full descriptions from JobStreet (ID/MY/PH/SG), JobsDB (TH/HK) and SEEK (AU/NZ) — keyword and location search with work-type, salary and date filters, plus direct job-URL lookup.

- **URL**: https://apify.com/scrapyx/jobstreet-jobs-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.35 / 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## JobStreet / JobsDB / SEEK Jobs Scraper

Extract job listings — with the **complete job description** — from JobStreet
(Indonesia, Malaysia, Philippines, Singapore), JobsDB (Thailand, Hong Kong)
and SEEK (Australia, New Zealand). One actor, eight markets, one output shape.

### Why use this actor

- **No account, no login, no API key.** Point it at a keyword and run.
- **Eight countries from one actor.** Pick the market from a dropdown — the
  data comes back in the same shape every time, so a multi-country pipeline is
  one schedule instead of eight.
- **Full job ads, not just cards.** Every row can carry the complete
  description, the employer's screening questionnaire, benefit bullets,
  advertiser identity and the exact salary band the employer published.
- **Filters that match the site's own:** keyword, location, work type,
  on-site/hybrid/remote, salary range, industry classification, and
  "posted in the last N days" — ideal for a scheduled run that only picks up
  new postings.
- **Direct job lookup.** Already have job URLs or IDs? Feed them in and skip
  search entirely — useful for re-checking whether a role is still open.
- **Stable JSON for pipelines.** Every input always produces at least one row,
  including a labelled row when something goes wrong, so joins downstream
  never silently lose records. Export to JSON, CSV or Excel.

### How it works

1. You choose a market and give it search keywords (and optionally a location
   and filters), or a list of job URLs.
2. The actor runs each keyword as its own search and pages through the
   results until it hits your item cap or the listings run out.
3. If full descriptions are enabled, it then fetches the complete ad for
   every job it found.
4. Each job is written to the dataset as one row, together with a summary row
   per search telling you how many matches the site reported and whether your
   location filter was understood.

Retries and back-off are handled for you. There are no scrapers, browsers or
blocks to babysit.

### Input

```json
{
  "country": "ID",
  "keywords": ["data analyst"],
  "location": "Jakarta",
  "maxItemsPerQuery": 100,
  "includeJobDetails": true,
  "sortBy": "ListedDate",
  "postedWithinDays": 7,
  "workTypes": ["Full time"],
  "workArrangements": ["Remote", "Hybrid"],
  "salaryMin": 10000000,
  "salaryType": "monthly",
  "maxConcurrency": 4
}
```

| Field | Type | Description |
|---|---|---|
| `country` | string | Market to search: `ID`, `MY`, `PH`, `SG` (JobStreet), `TH`, `HK` (JobsDB), `AU`, `NZ` (SEEK). Default `ID`. |
| `keywords` | array | One search per entry. Leave empty to browse everything in the market. |
| `location` | string | Optional location, e.g. `Jakarta`, `Indonesia`, `Kuala Lumpur`, `Sydney`. |
| `jobUrls` | array | Scrape specific ads directly — a full job URL or a bare numeric job ID. Can be combined with a keyword search. |
| `maxItemsPerQuery` | integer | Stop each search after this many jobs. `0` = unlimited. Default `100`. |
| `includeJobDetails` | boolean | Fetch the full description for every result. Turn off for a fast listing-only crawl. Default `true`. |
| `sortBy` | string | `KeywordRelevance` (default) or `ListedDate` (newest first). |
| `postedWithinDays` | integer | Only jobs listed in the last N days — `1` for the last 24 hours, `7` for the last week. Any day count works. `0` = any date. |
| `workTypes` | array | Any of `Full time`, `Part time`, `Contract/Temp`, `Casual/Vacation`. |
| `workArrangements` | array | Any of `On-site`, `Hybrid`, `Remote`. |
| `classificationIds` | array | Industry classification IDs, e.g. `1209` (Engineering). Every returned job carries its own IDs, so run once without this to discover them. |
| `salaryMin` / `salaryMax` | integer | Salary bounds in local currency. Setting either excludes ads published without a salary. |
| `salaryType` | string | `monthly` (Asian markets) or `annual` (AU/NZ). |
| `maxConcurrency` | integer | Requests in flight at once. Default `4`. |
| `proxyConfiguration` | object | Optional. Residential is recommended for large or scheduled runs. |

#### Direct job lookup

```json
{
  "country": "ID",
  "jobUrls": [
    "https://id.jobstreet.com/job/93908556",
    "93911766"
  ],
  "includeJobDetails": true
}
```

### Output

Every row carries `recordType`: `JOB` for a job, `SEARCH_SUMMARY` once per
search, `ERROR` when an input could not be processed.

#### `JOB` — real output, truncated for length

```json
{
  "_input": "ID | keywords='data analyst' | where='Jakarta'",
  "_source": "S1-search+S2-job-detail",
  "_scrapedAt": "2026-08-11T16:20:57Z",
  "recordType": "JOB",
  "country": "ID",
  "brand": "JobStreet",
  "jobId": "93911766",
  "jobUrl": "https://id.jobstreet.com/job/93911766",
  "searchContext": { "keywords": "data analyst", "where": "Jakarta", "page": 1, "rank": 1 },
  "title": "Data Analyst - Staff",
  "companyName": "Erajaya",
  "advertiser": { "id": "60216256", "description": "Erajaya Group" },
  "employer": {
    "id": "519523",
    "name": "Erajaya",
    "companyId": "176596129818055",
    "companyUrl": "https://id.jobstreet.com/companies/erajaya-176596129818055"
  },
  "classifications": [
    {
      "classification": { "id": "6251", "description": "Administration & Office Support" },
      "subclassification": { "id": "6255", "description": "Data Entry & Word Processing" }
    }
  ],
  "locations": [
    { "label": "Jakarta", "countryCode": "ID", "seoHierarchy": [{ "contextualName": "Jakarta" }] }
  ],
  "salaryLabel": "",
  "workTypes": ["Full time"],
  "workArrangements": { "data": [{ "id": "1", "label": { "text": "On-site" } }] },
  "listingDate": "2026-08-11T14:05:39Z",
  "listingDateDisplay": "2h ago",
  "teaser": "Advance in data formulation in Excel\nHaving exposure to SQL will be an advantage\ncapable of speaking in English",
  "roleId": "Data-Analyst-Staff",
  "isFeatured": false,
  "jobDetails": {
    "job": {
      "id": "93911766",
      "title": "Data Analyst - Staff",
      "abstract": "Advance in data formulation in Excel\nHaving exposure to SQL will be an advantage…",
      "content": "<div><div><p>Job Description</p></div></div>\n<div><div><ul>\n<li>Make weekly, monthly, quarterly &amp; yearly reports using Canva/ PowerPoint</li>\n<li>Doing Analytical &amp; insights for the department</li>\n<li>Helping the team with administration, especially in relation to payment &amp; purchase</li>\n</ul></div></div>\n<div><div><p>Job Requirements</p></div></div>…",
      "status": "Active",
      "isExpired": false,
      "expiresAt": { "dateTimeUtc": "2026-08-31T14:05:00.000Z" },
      "listedAt": { "dateTimeUtc": "2026-08-11T14:05:39.000Z", "label": "2h ago" },
      "salary": null,
      "workTypes": { "label": "Full time" },
      "location": { "label": "Jakarta" },
      "advertiser": { "id": "60216256", "name": "Erajaya Group", "isVerified": null },
      "classifications": [{ "label": "Data Entry & Word Processing (Administration & Office Support)" }],
      "products": { "branding": null, "bullets": null, "questionnaire": null },
      "shareLink": "https://id.jobstreet.com/job/93911766?tracking=SHR-WEB-SharedJob-asia-4"
    },
    "companySearchUrl": "https://id.jobstreet.com/Erajaya-jobs/at-this-company",
    "companyTags": []
  }
}
```

| Field | Type | Description |
|---|---|---|
| `jobId` / `jobUrl` | string | Job identifier and its public page. |
| `title`, `companyName` | string | Role title and the employer name shown in results. |
| `advertiser`, `employer` | object | Who posted the ad, plus the employer's company page when it has one. |
| `classifications` | array | Industry and sub-industry, with the IDs you can feed back into `classificationIds`. |
| `locations` | array | Location label, country code, and the location hierarchy. |
| `salaryLabel` | string | Salary exactly as the employer published it (empty when undisclosed). |
| `workTypes`, `workArrangements` | array/object | Full time / Part time / Contract, and On-site / Hybrid / Remote. |
| `listingDate`, `listingDateDisplay` | string | Exact posting timestamp, plus the site's relative label. |
| `teaser` | string | The short summary shown in search results. |
| `searchContext` | object | Which keyword, location, page and rank produced this row. |
| `jobDetails.job.content` | string | **The complete job ad** as HTML. Present when `includeJobDetails` is on. |
| `jobDetails.job.products.questionnaire` | object | The employer's screening questions, when the ad has them. |
| `jobDetails.job.products.bullets` | array | Benefit/perk bullets from the ad. |
| `jobDetails.job.expiresAt`, `status`, `isExpired` | — | Whether the role is still open and when it closes. |
| `jobDetails.companySearchUrl` | string | Link to every other open role at the same company. |

#### `SEARCH_SUMMARY`

```json
{
  "_input": "ID | keywords='data analyst' | where='Jakarta'",
  "recordType": "SEARCH_SUMMARY",
  "country": "ID",
  "brand": "JobStreet",
  "host": "id.jobstreet.com",
  "totalCount": 1423,
  "returnedCount": 2,
  "pagesFetched": 1,
  "locationResolved": true,
  "resolvedLocation": {
    "description": "Jakarta",
    "type": "State",
    "whereId": "2030500",
    "localisedDescriptions": { "en": { "contextualName": "Jakarta" }, "id": { "contextualName": "Jakarta Raya" } }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `totalCount` | integer | How many matches the site reports for this search. |
| `returnedCount` | integer | How many jobs this run actually collected. |
| `pagesFetched` | integer | How many result pages were read. |
| `locationResolved` | boolean | Whether your `location` was understood. **Check this first if a search returns nothing** — an unrecognised place name returns zero results rather than an error. |
| `resolvedLocation` | object | The exact place the site matched your text to. |

#### `ERROR`

One row per input that could not be processed, so nothing disappears
silently: `_error` is one of `invalid_input`, `detail_fetch_failed`,
`fetch_failed`, `unexpected_shape`, `upstream_error`, and `_errorDetail`
explains it in plain text.

### Notes and limits

- **Search depth.** The site caps its own result window at roughly 6,000 jobs
  per query, no matter how many matches it reports. To go deeper, split a
  broad search into several narrower ones (by location, classification, or
  `postedWithinDays`) instead of raising `maxItemsPerQuery`.
- **`totalCount` is approximate.** It is the site's own headline number and
  runs slightly ahead of the distinct jobs actually served — a search
  reporting 126 returned 122 unique roles. `returnedCount` is the honest one.
- **Several keywords may return the same job.** Each keyword is a separate
  search with its own rows, so a role matching two keywords appears twice with
  a different `searchContext`. Deduplicate on `jobId` if you want one row per
  role.
- **Salary is only as good as the ad.** Many employers publish no salary;
  those rows have an empty `salaryLabel`. Setting `salaryMin`/`salaryMax`
  excludes them entirely.
- **Speed.** Listing-only runs are fast (100 jobs per request). Turning on
  full descriptions adds one request per job — raise `maxConcurrency` for
  large runs, and lower it if the log starts showing rate limiting.
- A proxy is optional. Residential is recommended for large or scheduled runs.

### Related actors

| Actor | What it does |
|---|---|
| `agoda-reviews-scraper` | Guest reviews for any Agoda hotel |
| `booking-reviews-scraper` | Guest reviews for any Booking.com property |
| `tripcom-reviews-scraper` | Guest reviews for any Trip.com hotel |
| `getyourguide-reviews-scraper` | Traveller reviews for GetYourGuide activities |
| `viator-reviews-scraper` | Traveller reviews for Viator tours |
| `holidaycheck-reviews-scraper` | Guest reviews for HolidayCheck hotels |
| `priceline-reviews-scraper` | Guest reviews for Priceline hotels |

# Actor input Schema

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

Which country site to search. JobStreet (Indonesia, Malaysia, Philippines, Singapore), JobsDB (Thailand, Hong Kong) and SEEK (Australia, New Zealand) all run on the same platform, so every market returns the identical data shape.

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

One search per entry — each keyword is run as its own query and gets its own SEARCH\_SUMMARY row. Leave empty to browse every job in the market (optionally narrowed by Location and the filters below).

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

Optional location filter, resolved server-side against the market's own taxonomy — e.g. 'Jakarta', 'Indonesia', 'Kuala Lumpur', 'Sydney'. If a spelling does not resolve, the API silently returns 0 results: check `locationResolved` on the SEARCH\_SUMMARY row to tell 'no matches' apart from 'wrong spelling'.

## `jobUrls` (type: `array`):

Scrape specific job ads directly, skipping search entirely. Accepts a full job URL (https://id.jobstreet.com/job/93908556) or a bare numeric job id. Can be combined with a keyword search in the same run.

## `maxItemsPerQuery` (type: `integer`):

Stop paginating a query after this many jobs. The API serves 100 per page, so 100 = one request. Set to 0 for unlimited — note the platform caps its own result window at roughly 6,000 jobs per query regardless of how many matches it reports, so narrow the query (or add a Location / classification filter) to reach deeper inventory.

## `includeJobDetails` (type: `boolean`):

Fetch the complete job ad (full HTML description, screening questionnaire, benefit bullets, advertiser record) for every result via one extra request per job. Turn this off for a fast listing-only crawl — search cards already carry title, company, salary label, location, classification and a short teaser.

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

Relevance is the site default. Date sorts newest-first, which is what you want for a scheduled run that only needs new postings.

## `postedWithinDays` (type: `integer`):

Only return jobs listed in the last N days — 1 for the last 24 hours, 7 for the last week, and so on. Any day count works, not just the presets the website offers. Leave at 0 for any date.

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

Filter by employment type. Leave empty for all types.

## `workArrangements` (type: `array`):

Filter by on-site / hybrid / remote. Leave empty for all arrangements.

## `classificationIds` (type: `array`):

Filter by SEEK industry classification or sub-classification id (e.g. 1209 = Engineering, 6281 = Information & Communication Technology, 1203 = Banking & Financial Services). Every returned job carries its own ids under `classifications`, so run once without this filter to discover the ids that matter to you.

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

Lower bound in the market's local currency, matched against the salary band the advertiser published. Jobs advertised without a salary are excluded once this is set.

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

Upper bound in the market's local currency. Leave empty for no upper bound.

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

Whether the salary bounds above are monthly or annual figures. Asian markets advertise monthly; Australia and New Zealand advertise annual.

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

Upper bound on requests in flight at once, across search paging and job-detail fetches. Raise it for large runs; lower it if you start seeing HTTP 429 in the log.

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

Residential, pinned to the market you are scraping. Recon from a home connection saw no IP gating, but cloud runs are a different posture: from Apify's own container IP, Cloudflare challenges the market homepage every time, costing ~22s per run before any data is fetched. With Residential the same request succeeds immediately. Turning the proxy off will still return data — the search API itself is not gated — but each run pays that fixed delay.

## Actor input object example

```json
{
  "country": "ID",
  "keywords": [
    "data analyst"
  ],
  "location": "",
  "jobUrls": [],
  "maxItemsPerQuery": 100,
  "includeJobDetails": true,
  "sortBy": "KeywordRelevance",
  "postedWithinDays": 0,
  "workTypes": [],
  "workArrangements": [],
  "classificationIds": [],
  "salaryType": "monthly",
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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": [
        "data analyst"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/jobstreet-jobs-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 = { "keywords": ["data analyst"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/jobstreet-jobs-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 '{
  "keywords": [
    "data analyst"
  ]
}' |
apify call scrapyx/jobstreet-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/jobstreet-jobs-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/npuXp0zh2OAIcmJmD/builds/elfp8eeghvUfa7qjL/openapi.json
