JobStreet $1๐ฐ Keyword & URL Job Scraper
Pricing
from $1.00 / 1,000 results
JobStreet $1๐ฐ Keyword & URL Job Scraper
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, browser-free, and cost-efficient.
Pricing
from $1.00 / 1,000 results
Rating
5.0
(1)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
15
Total users
5
Monthly active users
9 hours ago
Last modified
Categories
Share
JobStreet Jobs Scraper (Malaysia, Singapore, Philippines, Indonesia)
Scrape job listings from JobStreet across all four Southeast Asian markets: Malaysia (my.jobstreet.com), Singapore (sg.jobstreet.com), the Philippines (ph.jobstreet.com), and Indonesia (id.jobstreet.com). The actor reads JobStreet's public JSON search and returns one clean, flat record per job, with an optional full description, company overview, salary, and contact details pulled from the public job-detail endpoint. No browser, fast, and inexpensive.
Search two ways: fill in the filter fields (country, keywords, location, category, work type, salary, date range), or paste one or more JobStreet search URLs and let the actor read the filters straight from each link.
Why this scraper
- Covers all four JobStreet country sites from one actor, with the country auto-detected from each pasted URL.
- 60+ 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 fetches the full description for a whole page of up to 22 jobs, so deep runs stay fast.
- Forward pagination across as many pages as you allow, with automatic de-duplication by job ID.
- Filter by category and subcategory, work type, salary range (monthly or annual), and listing recency.
- Global caps on item count and run time, so a run never overshoots your budget. Partial results are always flushed.
Data you get
Sample shape, values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
id | "00000001" |
url | "https://my.jobstreet.com/job/00000001" |
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 ..." |
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=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 |
applyLink | "https://my.jobstreet.com/job/00000001/apply" |
scrapedAt | "2026-01-01T00:00:00.000Z" |
The full record carries more: classifications[] with IDs, workArrangementLabels, salaryCurrency, shareLink, companyOpenJobs, branding, advertiser details, and raw passthrough blocks (advertiserRaw, employerRaw, companyProfileRaw) so no upstream field is ever dropped. detailFetched tells you, per record, whether the full-description graphql fetch actually succeeded for that job (only meaningful when includeFullDescription is on). changeType, changedFields, firstSeenAt, and lastSeenAt are added only when incrementalMode is on โ see "Resume and recurring updates" below.
How to use
1. Basic search (Malaysia)
{"country": "MY","keywords": "software engineer","location": "Kuala Lumpur","maxItems": 50}
2. Search with filters (Singapore, ICT, full time, salary range)
{"country": "SG","keywords": "data engineer","classification": ["6281"],"workType": ["242"],"salaryMin": 5000,"salaryMax": 12000,"salaryType": "monthly","daterange": 14,"sortmode": "ListedDate","maxItems": 100,"includeFullDescription": true}
3. Multiple countries via URL mode
{"urls": ["https://my.jobstreet.com/it-jobs/in-Penang","https://sg.jobstreet.com/jobs-in-information-communication-technology","https://ph.jobstreet.com/accountant-jobs","https://id.jobstreet.com/marketing-jobs/in-Jakarta-Raya"],"maxItems": 200,"includeFullDescription": false}
In URL mode, the filter fields in Option 1 are ignored. Each URL's host sets the country, and its path and query string set the filters. You can mix all four country sites in one run.
4. Recent jobs only, sorted by date
{"country": "ID","keywords": "marketing","daterange": 7,"sortmode": "ListedDate","maxItems": 30}
Input parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
country | string | "MY" | Market to search: MY, SG, PH, or ID. Applies in filter mode only. |
keywords | string | (empty) | Job title or keywords. Empty matches all jobs in the location. |
location | string | (empty) | Location within the country, for example Kuala Lumpur or Jakarta Raya. Empty searches the whole country. |
classification | array | [] | One or more category IDs. Empty means all categories. |
workType | array | [] | Work types: 242 Full time, 243 Part time, 244 Contract/Temp, 245 Casual/Vacation. |
salaryMin | integer | (none) | Minimum salary in local currency. |
salaryMax | integer | (none) | Maximum salary in local currency. |
salaryType | string | "monthly" | Whether salary values are monthly or annual. |
daterange | integer | (none) | Listed within N days. Accepted: 1, 3, 7, 14, 31. |
sortmode | string | "ListedDate" | ListedDate newest first, or KeywordRelevance best match. |
urls | array | [] | One or more JobStreet search URLs. Overrides Option 1. Country auto-detected per URL. |
includeFullDescription | boolean | true | Fetch the full HTML description, company overview, expiry, and phone via the job-detail endpoint. Batched per page. |
maxItems | integer | 20 | Global cap across all URLs and pages. 0 means unlimited. |
maxTimeSec | integer | 0 | Global wall-clock cap in seconds. 0 means no limit. Partial results are flushed when reached. |
resumeFromRunId | string | (empty) | Continue one specific interrupted run. See "Resume and recurring updates" below. |
incrementalMode | boolean | false | Track a scheduled search across runs and classify changes. See below. |
stateKey | string | (empty) | Optional name for the Incremental mode baseline. Auto-derived when empty. |
emitUnchanged | boolean | false | Also push rows unchanged since the last Incremental mode run. Bills extra rows. |
emitExpired | boolean | false | Push a synthetic row for jobs that dropped out of a fully-scanned search. Bills extra rows. |
proxyConfiguration | object | Apify datacenter | Proxy settings. See below. |
Resume and recurring updates
Resume (resumeFromRunId) continues one specific interrupted run: paste the run ID or dataset ID from a previous run and this one skips every job already collected there, picking up only what's still missing.
Incremental mode (incrementalMode) is for a search you run on a schedule (daily/weekly) against the same keywords/location/filters. Turn it on and the actor remembers what the search returned last time and classifies every job as:
NEWโ not seen beforeUPDATEDโ content changed since last time (seechangedFields)UNCHANGEDโ identical to last timeREAPPEAREDโ was gone, is backEXPIREDโ tracked before, no longer found (only after a run that scanned every page of every search with nothing capping, time-limiting, or interrupting it โ a partial scan can't tell "gone" from "not reached yet")
Off by default, so every existing scheduled run is unaffected. stateKey names the saved baseline; leave it empty and the actor derives one automatically from country/keywords/location/classification/workType/salaryType/salaryMin/salaryMax/daterange/sortmode/urls/includeFullDescription, so two differently-configured searches never share a baseline by accident.
By default, only NEW/UPDATED/REAPPEARED/EXPIRED rows are pushed. Turning on emitUnchanged or emitExpired bills extra rows (the per-result event, plus the detail-enrichment surcharge if includeFullDescription is on) for the additional rows those toggles push โ leave them off if you only want to pay for what actually changed.
A note on maxItems with Incremental mode on: the cap counts rows actually pushed, not scanned. If most of a search is UNCHANGED and suppressed, the actor keeps paginating to backfill your maxItems budget with fresh listings โ a capped incremental run can still return maxItems rows even when almost everything on the tracked pages didn't change. EXPIRED detection only ever fires on an uncapped, un-timed-out, non-resumed run that reached the natural end of every search.
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.
- Authorize a connector once under Apify โ Settings โ Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto 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.
Output example
Sample shape, values are illustrative placeholders, not from a live listing.
{"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=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,"scrapedAt": "2026-01-01T00:00:00.000Z"}
Plan requirement and proxy
The actor defaults to Apify datacenter proxy, which is included on the free plan, and JobStreet serves its public data fine through it. If your account cannot reach datacenter proxy, the actor automatically falls back to a backup pool, then to a direct connection, so a run never fails just because of proxy access.
For heavy or production workloads, residential proxy gives more rotation and reliability. Set proxyConfiguration.apifyProxyGroups to ["RESIDENTIAL"] (available on Apify paid plans) and optionally pin the country with apifyProxyCountry to match the market you are scraping.