JobStreet Scraper - Jobs, Salaries, Companies & Apply Details
Pricing
from $1.00 / 1,000 results
JobStreet Scraper - Jobs, Salaries, Companies & Apply Details
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.
Pricing
from $1.00 / 1,000 results
Rating
5.0
(1)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
18
Total users
5
Monthly active users
4 days ago
Last modified
Categories
Share
JobStreet Jobs Scraper: Job Listings Across Malaysia, Singapore, the Philippines and Indonesia
JobStreet Jobs Scraper turns JobStreet into a clean job-search API across all four Southeast Asian markets it runs: Malaysia, Singapore, the Philippines and Indonesia. Search by keywords, location, category, work type, salary and listing date, or paste JobStreet search URLs directly and let the actor read the filters from the link. Get back structured job records with the full HTML description, company overview, salary and any contact details the ad exposes, then export to JSON, CSV or Excel, or pull results straight into your app through the API.
Why This Scraper?
- 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.
- Global caps on item count and run time, so a run never overshoots your budget, and whatever was already fetched is always saved even if a cap or deadline stops the run early.
Use Cases
- 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.
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" |
applyLink | "https://my.jobstreet.com/job/00000001/apply" |
title | "Software Engineer" |
teaser | "Join a growing team building cloud services." |
companyName | "Sample Company Sdn Bhd" |
companyOverview | "Sample Company is a leading provider of cloud services in the region." (when includeFullDescription is on) |
advertiserName | "Sample Company Sdn Bhd" |
locationLabel | "Kuala Lumpur" |
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.</p>" (when includeFullDescription is on) |
contentText | "Full job description, plain text." (when includeFullDescription is on) |
bulletPoints | ["Competitive salary", "Hybrid work", "Career growth"] |
phoneNumbers | ["+60100000000"] (when includeFullDescription is on) |
emails | ["careers@example.com"] (when includeFullDescription is on) |
listingDate | "2026-01-01T00:00:00Z" |
listingDateDisplay | "2d ago" |
expiresAt | "2026-02-01T00:00:00Z" (when includeFullDescription is on) |
isVerified | true (when includeFullDescription is on) |
detailFetched | true |
scrapedAt | "2026-01-01T00:00:00.000Z" |
The full record carries more than this table: classifications[] with category IDs, workArrangementLabels, salaryCurrency, shareLink, companyOpenJobs, branding, advertiser details, and raw passthrough blocks (advertiserRaw, employerRaw, companyProfileRaw) so no upstream field is dropped. content, contentText, contentSections, companyOverview, expiresAt, isVerified, salaryCurrency, phoneNumbers and emails only come from the detail fetch and stay empty when includeFullDescription is off, while detailFetched tells you, per job, whether that fetch actually succeeded. When incrementalMode is on, every row also carries changeType, changedFields, firstSeenAt and lastSeenAt, and a job that drops out of a fully scanned search can appear as a synthesized EXPIRED row built from its last known snapshot, see "Resume and recurring updates" below for exactly when that happens.
How to Use
- Pick how you want to search: fill in the filter fields (country, keywords, location, category, work type, salary, listing date) under Option 1, or paste one or more JobStreet search URLs under Option 2. If you set
urls, every Option 1 field is ignored, and the country and filters come from each URL instead. - Turn
includeFullDescriptionon to get the full HTML description, company overview, expiry date and phone number for each job. Leave it off to get only what the search results themselves already carry, without the extra per-page request. - Set
maxItemsand, optionally,maxTimeSecto cap the size and length of the run, then click Start. - Download the dataset as JSON, CSV or Excel, or read it through the API.
Basic search (Malaysia):
{"country": "MY","keywords": "software engineer","location": "Kuala Lumpur","maxItems": 50}
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}
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}
Recent jobs only, sorted by date:
{"country": "ID","keywords": "marketing","daterange": 7,"sortmode": "ListedDate","maxItems": 30}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/jobstreet-scraper").call(run_input={"country": "MY","keywords": "software engineer","location": "Kuala Lumpur","maxItems": 50,})for job in client.dataset(run["defaultDatasetId"]).iterate_items():print(job["title"], job["companyName"], job["salaryLabel"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/jobstreet-scraper').call({country: 'MY',keywords: 'software engineer',location: 'Kuala Lumpur',maxItems: 50,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
How caps shape a run
maxItems is a single budget shared across every URL and every page, not a per-URL limit: with 3 URLs and maxItems=50, the actor paginates the first URL until the budget is used up, and only moves to the second and third URL if room remains. Set maxItems to 0 for no cap. maxTimeSec is a wall-clock budget: once it elapses, the actor stops paginating, flushes whatever it already fetched, and ends the run cleanly instead of losing that work. Leaving both maxItems and maxTimeSec at 0 runs until the search itself runs out of pages, so set at least one of them for a predictable run.
Resume and recurring updates
Resume (resumeFromRunId) continues one interrupted run: paste its run ID or dataset ID and the actor skips every job it already collected there, so a retried run adds only what is still missing.
Incremental mode (incrementalMode) is for a search you run on a schedule against the same filters or URLs. Turn it on and the actor remembers what that exact search returned last time, keyed by stateKey (or an automatic key derived from your filters, so two differently configured searches never share a baseline by accident). Every job scraped this run is then classified:
NEW: not seen in a previous run.UPDATED: content changed since last time, withchangedFieldsnaming exactly what.UNCHANGED: identical to last time. Suppressed by default; turn onemitUnchangedto also push (and pay for) these rows.EXPIRED: was tracked before and is missing from this run. This only fires when the run reached the natural end of every search it ran, with nomaxItemscap, nomaxTimeSecdeadline and noresumeFromRunIdcutting it short, since a partial scan cannot tell a job is gone from a job it simply has not reached yet. It also requiresemitExpiredto be on; otherwise the previous state for those jobs is left untouched and nothing is emitted.REAPPEARED: a job that was previously markedEXPIREDshows up again. Because a job can only becomeEXPIREDwhenemitExpiredwas on for a prior complete scan,REAPPEAREDcan only occur under that same condition. If you never turnemitExpiredon, a job that drops out of the results and later comes back is simply classifiedUNCHANGEDorUPDATEDagainst its last snapshot, neverREAPPEAREDorNEW.
Incremental mode is off by default, so existing scheduled runs are unaffected. maxItems still counts rows actually pushed, not rows scanned: if most of a tracked search is UNCHANGED and suppressed, the actor keeps paginating to backfill your budget with jobs that did change.
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, through Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape. The Apify dataset itself is never changed.
What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each job 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 have not authorized a connector yet.
- For Notion, also set
notionParentPageUrlto the page where job pages 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.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
country | string | "MY" | Market to search: MY, SG, PH, or ID. Applies in filter mode only. |
keywords | string | none (prefill: "software engineer") | Job title or keywords. Empty matches all jobs in the location. |
location | string | none | Location within the country, for example Kuala Lumpur or Jakarta Raya. Empty searches the whole country. |
classification | array | [] | One or more top-level 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 the local currency. |
salaryMax | integer | none | Maximum salary in the local currency. |
salaryType | string | "monthly" | Whether the salary filter values are monthly or annual. |
daterange | integer | none | Listed within N days. Accepted: 1, 3, 7, 14, 31. Other values are dropped with a warning. |
sortmode | string | "ListedDate" | ListedDate newest first, or KeywordRelevance best match. |
urls | array | [] | One or more JobStreet search URLs. Overrides every filter field above. Country is auto-detected per URL. |
includeFullDescription | boolean | true | Fetch the full HTML description, company overview, expiry date 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 the cap is reached. |
resumeFromRunId | string | none | Continue one specific interrupted run. See "Resume and recurring updates" above. |
incrementalMode | boolean | false | Track a scheduled search across runs and classify changes. See above. |
stateKey | string | none | Optional name for the Incremental mode baseline. Auto-derived from your filters 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 proxy, default connection group | Proxy settings. See "Plan Requirement" below. |
mcpConnectors | array | none | Optional: send a summary of each job to apps you authorized under Integrations. |
notionParentPageUrl | string | none | Notion connector only: the page under which job pages are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
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 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"}
Plan Requirement
The default proxy setting works out of the box for JobStreet's public search and detail endpoints, and the actor automatically tries a backup connection if the primary one is refused, then continues directly as a last resort, so a run does not fail purely because of a blocked connection. For large or frequent runs across many pages, the residential proxy group gives more headroom; pick it under Connection and optionally pin the country to match the market you are scraping.
FAQ
How much does it cost?
You pay per job returned, with the full-detail fetch billed only when includeFullDescription is on, and Incremental mode's emitUnchanged or emitExpired billed only for the extra rows those toggles add. The Pricing tab shows the current rates. Use maxItems to cap the cost of any run.
Is it legal to scrape JobStreet?
This actor collects only publicly available job listing data. You are responsible for how you use it: follow JobStreet's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Job listings are generally public information, but company logos and branding may carry separate rights.
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.
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.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
🔗 Want more JobStreet data?
Pair this actor with these related scrapers from the same team:
| 📇 JobStreet From $1/1K. Pull JobStreet company profiles and employee reviews across Malaysia... | 💼 Jora Jobs Scraper Scrape Jora.com job listings across supported countries by keyword, filters, or URL... |
| 💼 SEEK Jobs Scraper From $0.7/1K. Scrape SEEK.com.au and SEEK.co.nz jobs by keyword, location, or filters... | 💼 ZipRecruiter Scraper From $1/1K. Scrape ZipRecruiter jobs with titles, companies, salaries, locations... |
| 💼 HH.ru Jobs Scraper From $1/1K. Scrape HH.ru job listings with 50+ structured fields. Search by filters or... | 💼 Workday Jobs Scraper Scrape job postings from any Workday-hosted careers site on Myworkdayjobs.com. Paste... |
💬 Support & custom scrapers
- 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- 🛠️ Need another site, extra fields or a private build? Email abotapi@proton.me or message Telegram @abotapi.
- ⭐ Enjoying it? A quick review on the actor page helps other users find it.