JobStreet Scraper icon

JobStreet Scraper

From $1/1K. Scrape JobStreet listings across Malaysia, Singapore, Indonesia, and the Philippines. Extract titles, companies, salaries, locations,…

Start using this actor on Apify ↗Need a custom scraper?Contact us ↗

JobStreet Scraper at a glance

JobStreet Scraper is a ready-to-run Apify actor from abotapi for collecting jobs data. Its documented output includes title, company, location, salary and listed, which you can export as JSON, CSV or Excel or read through the Apify API. It costs $1.70 per 1,000 results on the Apify Free plan and from $1.00 on paid plans, with smaller per-run or optional feature charges listed on Apify.

Runs on
Apify cloud: console, API, schedules and integrations
Data type
Jobs, Automation, Developer Tools
Pricing
$1.70 per 1,000 results (Free plan), from $1.00 on paid plans
Output
Documented below · JSON, CSV, Excel
Inputs
33 options
Run success
100.0% of completed public runs in the last 30 days

From $1/1K. Scrape JobStreet listings across Malaysia, Singapore, Indonesia, and the Philippines. Extract titles, companies, salaries, locations, descriptions, company info, and apply details. Search with filters or paste JobStreet URLs directly. Fast and cost-efficient.

What JobStreet Scraper can do

  • All four JobStreet country sites from one actor. The country is auto-detected from each pasted URL, or picked from a dropdown when you search by filters.
  • Two ways to search. Fill in the filter fields (country, keywords, location, category, work type, salary range, listing date), or paste one or more JobStreet search URLs and mix all four markets in a single run.
  • 50-plus fields per job, including the full HTML description, plain-text body, bullet points, salary, company overview, and any phone numbers or emails found in the ad body.
  • Detail enrichment is batched. One extra request covers a whole page of up to 22 jobs, not one request per job, so a full run with descriptions costs far fewer requests than fetching each job's page individually.
  • Forward pagination across as many pages as you allow, with automatic de-duplication by job ID within a run.
  • Filter by category, work type, salary range and listing recency, in filter mode or through the URL itself.

Choose what to collect

These inputs let you configure JobStreet Scraper - Jobs, Salaries, Companies & Apply Details. Open the actor on Apify to enter your values and review the current options.

Supported input fields
InputWhat it controls
countrystringCountry market to search. MY = my.jobstreet.com, SG = sg.jobstreet.com, PH = ph.jobstreet.com, ID = id.jobstreet.com.

Choices: MY, SG, PH, ID

keywordsstringJob title or keywords (e.g. 'software engineer'). Leave empty to match all jobs in the location.
locationstringWhere to search within the country. Examples: 'Kuala Lumpur', 'Selangor', 'Singapore', 'Metro Manila', 'Jakarta Raya'. Leave empty to search the whole country.
classificationarrayFilter by one or more top-level job categories. Leave empty for all. Subclassifications can still be used via URL mode.
workTypearrayFilter by one or more work types. Leave empty for all.
workArrangementarrayFilter by on-site, hybrid or remote. Leave empty for all. Ignored in URL mode (put workarrangement in the URL instead).
subClassificationarrayNarrow to sub-categories by numeric ID (e.g. 6028 = Electrical/Electronic Engineering, shown in the output as subCategoryId). Use together with a category. Ignored in URL mode.
Show 26 more inputs
More input fields
InputWhat it controls
salaryMinintegerOptional minimum salary, in the local currency (RM / SGD / PHP / IDR). By default treated as a monthly figure.
salaryMaxintegerOptional maximum salary, in the local currency. By default treated as a monthly figure.
salaryTypestringWhether the salary filter values are monthly or annual amounts. JobStreet markets quote monthly salaries by default.

Choices: monthly, annual

daterangeintegerOnly return jobs listed in the last N days. Accepted values: 1, 3, 7, 14, 31. Other values are dropped with a warning.
includeKeywordsarrayPost-filter: keep only jobs whose title, company, teaser, bullet points or description (when 'Include full description' is on) mention at least one of these words.
excludeKeywordsarrayPost-filter: drop jobs that mention any of these words (same fields as above).
fromDatestringPost-filter, ISO date or datetime (e.g. 2026-10-01).
toDatestringPost-filter, ISO date or datetime.
maxAgeMinutesintegerPost-filter: keep only jobs listed within the last N minutes. 0 = off.
requireContactstringPost-filter: keep only jobs where an email and/or phone was found. Needs 'Include full description' on.

Choices: off, email, phone, either, both

sortmodestringListedDate = newest first. KeywordRelevance = best match.

Choices: ListedDate, KeywordRelevance

urlsarrayPaste one or more JobStreet search URLs. Supported: keyword and location pages (/software-engineer-jobs/in-Kuala-Lumpur), keyword-only (/it-jobs), location-only (/jobs/in-Selangor), category and sub-category pages (/jobs-in-information-communication-technology.../developers-programmers), a work type (full-time, part-time, contract-temp, casual-vacation) or work arrangement (on-site, hybrid, remote) after the keyword or category, the filtered query form (/jobs?classification=6281&worktype=242&daterange=7&page=2), a ?where= location, hosts my, sg, ph and id, or a v5 search API link. Job detail pages (/job/), single-job share links (?jobId=) and company pages are not supported and are skipped with a warning.
includeFullDescriptionbooleanIf true, fetches the full job HTML body, company overview, expiry date, and phone (when available) via the jobseeker graphql endpoint. Batched per page: one extra request covers up to 22 jobs.
maxItemsintegerTotal cap across ALL URLs and ALL pages (not per-URL). Example: maxItems=50 with 3 URLs paginates URL 1 until the budget is filled; URLs 2 and 3 run only if budget remains. Set to 0 for unlimited.
maxPagesintegerStop each search after N result pages (22 jobs per page). 0 = no limit.
descriptionFormatstringWhich description shapes to output: all (text, HTML, Markdown), or only one.

Choices: all, text, html, markdown

maxTimeSecintegerWall-clock cap across ALL URLs and pages. When exceeded, the actor stops paginating, flushes whatever it already fetched to the dataset, and exits cleanly. Set to 0 for no time limit.
resumeFromRunIdstringPaste a previous run ID or dataset ID to continue an interrupted crawl: jobs already collected there are skipped so you get only what's still missing. One-off continuation of a SPECIFIC run - for ongoing scheduled monitoring of the same search, use Incremental mode below instead.
incrementalModebooleanFor a search you run on a schedule (e.g. daily/weekly): remembers what this exact search returned last time and classifies every job as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED, so you can act on only what changed. Off by default so existing scheduled runs are unaffected. Separate from Resume above, which continues one specific interrupted run. Uses State key below to isolate different searches; see Emit unchanged rows / Emit expired rows to control which of those get pushed (both bill extra rows when on). A job is UPDATED when any returned field differs from last time except scrapedAt, listingDateDisplay (the site's relative 'N ago' text) and detailFetched, which are excluded because they change on every fetch; a genuine relist (a new listingDate) does count. Add more with Ignore fields when detecting changes.
stateKeystringNames the saved baseline Incremental mode compares against. Leave empty to auto-derive one from your country/keywords/location/classification/workType/salary/daterange/sortmode/urls/includeFullDescription - two differently-configured searches never share a baseline even with this left empty. Set your own text only if you want to explicitly reuse or separate baselines yourself. Ignored when Incremental mode is off.
emitUnchangedbooleanWhen Incremental mode is on: also push rows classified UNCHANGED (identical to last run), not just NEW/UPDATED/REAPPEARED/EXPIRED. Off by default to save you from paying for rows that didn't change. Turning this on bills the per-result event (and the detail-enrichment surcharge, if Fetch full job description is on) for every unchanged row too.
emitExpiredbooleanWhen Incremental mode is on: push a synthetic EXPIRED row for jobs tracked in a previous run that no longer appear in this one. Only fires after a run that scanned every page of every search with no cap, time limit, or resume truncating it - a partial scan can't tell 'gone' apart from 'not reached yet', so it's skipped (with a log line) otherwise. Off by default; turning it on bills the per-result event for each EXPIRED row emitted.
ignoreFieldsForChangesarrayOptional. Field names to exclude from UPDATED change detection when Incremental mode is on, e.g. salaryLabel or branding.logo (a dotted path reaches inside nested objects). The fields are still returned in every row; they just never turn a row into UPDATED or appear in changedFields. Changing this list never causes a one-time wave of UPDATED rows.
mcpConnectorsarrayOptionally 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).
notionParentPageUrlstringURL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.
maxNotifyListingsintegerCap on items written to each connector per run. Does not affect the dataset.

Fields in the output

The documented dataset includes the fields below. Availability can depend on the source page and selected mode.

titleTitle (text)
companyNameCompany (text)
locationLabelLocation (text)
salaryLabelSalary (text)
listingDateDisplayListed (text)
urlURL (link)
companyOverviewCompany overview (text)
contentTextDescription (text)
bulletPointsHighlights (array)
emailsEmails (array)
phoneNumbersPhones (array)
applyLinkApply link (link)
Show 4 more fields
changeTypeChange (text)
changedFieldsChanged fields (array)
firstSeenAtFirst seen (date)
lastSeenAtLast seen (date)

Example output

An example from this actor’s documentation. Values are illustrative; this is not a fresh live result.

{
  "id": "00000001",
  "url": "https://my.jobstreet.com/job/00000001",
  "applyLink": "https://my.jobstreet.com/job/00000001/apply",
  "title": "Software Engineer",
  "teaser": "Join a growing team building cloud services.",
  "companyName": "Sample Company Sdn Bhd",
  "companyId": "00000000",
  "companyOverview": "Sample Company is a leading provider of cloud services in the region.",
  "advertiserName": "Sample Company Sdn Bhd",
  "locationLabel": "Kuala Lumpur",
  "locationSeoHierarchy": [
    "Kuala Lumpur",
    "Wilayah Persekutuan"
  ],
  "countryCode": "MY",
  "classificationInfo": {
    "classification": "Information & Communication Technology",
    "subClassification": "Developers/Programmers"
  },
  "workTypes": [
    "Full time"
  ],
  "salaryLabel": "RM 5,000 to RM 8,000 per month",
  "content": "<p>Full job description HTML appears here when includeFullDescription is true.</p>",
  "contentText": "Full job description plain text appears here.",
  "bulletPoints": [
    "Competitive salary",
    "Hybrid work",
    "Career growth"
  ],
  "phoneNumbers": [
    "+60100000000"
  ],
  "emails": [
    "careers@example.com"
  ],
  "listingDate": "2026-01-01T00:00:00Z",
  "listingDateDisplay": "2d ago",
  "expiresAt": "2026-02-01T00:00:00Z",
  "isVerified": true,
  "detailFetched": true,
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}

Ways to use this data

  • Job boards and aggregators: mirror JobStreet listings into your own board or app across all four markets.
  • Recruiting and sourcing teams: monitor new roles in a category or location and route them into a pipeline or CRM.
  • Salary and market research: build salary benchmarks by market, category and work type from the structured salary and classification fields.
  • Lead generation and outreach: collect contact phone numbers and emails that employers left directly in the ad body.
  • Company intelligence: track a company's open roles and hiring pace over time.
  • Labor market analytics: build historical datasets of job counts, categories and locations across Malaysia, Singapore, the Philippines and Indonesia.

Before you run

Start with a small input and check the resulting records against your expected fields. Source content and actor options can change; the current Apify listing is the reference for availability and pricing.

Questions about this actor

What data does JobStreet Scraper return?

The documented output for JobStreet Scraper includes title, company, location, salary, listed, url, company overview and description. Availability of each field depends on the source page and the selected mode.

How much does JobStreet Scraper cost?

It costs $1.70 per 1,000 results on the Apify Free plan and from $1.00 on paid plans, with smaller per-run or optional feature charges listed on Apify. The Apify listing shows the current rate for every plan.

Can I get only new or changed jobs on a schedule?

Yes. Schedule the actor from the Schedules tab and turn on incrementalMode. Each run then classifies every job as NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED against what the same search returned last time, and unchanged jobs are not billed unless you turn emitUnchanged on.

Why are my filter fields being ignored?

URL mode always overrides filter mode. If urls has any entries, every field under Option 1 (country, keywords, location, category, work type, salary, date range) is ignored, and the actor reads the country and filters from each URL's own path and query string instead. Clear urls to go back to filter search. Behaviour change: the urls default is now one example URL per supported shape, so a run that leaves urls untouched and every other input on its default starts on those examples (the default maxItems of 20 ends it early). As soon as you set any filter field or incrementalMode to a non-default value, the untouched examples are ignored and your filter search runs (and keeps its previous incremental state key), so existing filter-mode callers see no change.

Why did my run fail instead of returning an empty dataset?

A search that legitimately matches nothing exits cleanly with a status message explaining why, and pushes no billable row, so "no jobs found" is never confused with a broken run. A run actually fails, instead, when resumeFromRunId points at a run or dataset ID that cannot be read, or when incrementalMode and resumeFromRunId are combined on a search that already has saved Incremental mode state: the actor stops with a clear message telling you to drop one of the two rather than silently mixing them.

What views does the dataset have?

The dataset has three table views in the Apify Console: Jobs Overview (title, company, location, salary, listed, URL), Job Details (title, company, company overview, description, highlights, emails, phones, apply link, URL) and Incremental Changes (title, change type, changed fields, first and last seen, URL). Each run also stores a run summary under the OUTPUT key of its key-value store.

Ready to collect with JobStreet Scraper?

Run it on Apify with your own input and export the results as JSON, CSV or Excel.