# JobStreet & JobsDB Scraper + Hiring Company Leads (Asia) (`snappytools/jobstreet-jobsdb-scraper`) Actor

Search JobStreet Malaysia, Singapore, Indonesia and the Philippines, and JobsDB Hong Kong and Thailand. One row per job with monthly salaries, plus one row per hiring company with website, emails and phone numbers.

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

## Pricing

from $1.00 / 1,000 jobs

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

## JobStreet & JobsDB Scraper + Hiring Company Leads (Asia)

Search JobStreet Malaysia, Singapore, Indonesia and the Philippines, and JobsDB Hong Kong and Thailand, the same way you would on the websites, and get every matching job as a clean table. Pick one country or all six in the same run.

Salaries are split into minimum, maximum, currency and pay period, with a monthly and a yearly figure added so ads in different formats can be compared ("RM 4,000 – RM 6,000 per month", "Rp 8.000.000", "S$120,000 a year").

It can also give you **one row per hiring company**: how many jobs they have open, what they're hiring for and where, their website, and (if you turn it on) the emails, phone numbers and social links from their website and job ads. Recruitment and staffing agencies are flagged, or you can leave them out.

Put it on a schedule with **Only new jobs since my last run** and each run returns only jobs you haven't seen before.

### Why this one

- **Six markets, one tool.** my.jobstreet.com, sg.jobstreet.com, id.jobstreet.com, ph.jobstreet.com, hk.jobsdb.com and th.jobsdb.com. Each search line can run in every country you pick.
- **Hiring companies as leads, not just job rows.** Twenty ads from the same company become one row with their website, emails, phone numbers and socials. In a real test (accountant jobs in Singapore, 17 Oct 2026), 9 of 17 companies came back with an email or phone number.
- **Contact details from the company's own website.** Not only what's written in the ad: the tool finds their site (company profile, the ad, or their name) and checks the homepage and contact page. Phone numbers come back in international format (+60, +65, +62, +63, +852, +66), including WhatsApp links.
- **Leave out recruitment agencies** so your list is companies hiring for themselves. Agencies are spotted from their name and, for company rows, from the industry on their company profile.
- **Pay once per lead.** "Only companies I haven't had before" skips companies an earlier run already gave you.
- **Salaries you can compare.** Monthly and yearly figures in the ad's currency, with the period worked out when the ad doesn't say.
- **$1 per 1,000 jobs, and no real per-run fee** ($0.00005 per run). No charge for empty searches or status rows.

### Quick start: a lead list in one run

Paste this into the Input tab (JSON view) and click Start. Change the search and countries to your own market.

```json
{
    "searches": ["accountant"],
    "countries": ["my", "sg"],
    "output": "companies",
    "findCompanyContacts": true,
    "skipRecruiters": true,
    "datePosted": "14",
    "maxJobsPerSearch": 200
}
```

You get one row per company that advertised an accountant job in Malaysia or Singapore in the last two weeks, with their contact details. Turn on **Only companies I haven't had before** and schedule it weekly to get only new companies each time.

### What you can use it for

| Who | What for |
| --- | --- |
| B2B sales teams | A company that's hiring is growing or changing. Find businesses hiring for roles your product supports (for example accounts, IT support, drivers) and contact them while it's relevant |
| Recruitment agencies | Find employers advertising directly, without an agency, and offer to help. Turn on "Leave out recruitment agencies" |
| Job boards and aggregators | A daily feed of new jobs in your niche, country or city |
| Salary research | Advertised salaries by role, classification and location, as monthly and yearly figures |
| Market and workforce research | Hiring demand across six countries by classification, location, work type and remote/hybrid |

### How to use it

1. Click **Try for free**.
2. In **Countries**, pick one or more: Malaysia, Singapore, Indonesia, Philippines, Hong Kong, Thailand.
3. In **Searches**, add one search per line: the keywords, then a `|` and a location. For example:
   - `accountant | Kuala Lumpur`
   - `software engineer | Singapore`
   - `sales | Jakarta Raya`
   - `customer service | Metro Manila`
   - `nurse` (no location means the whole country)
   - Or paste the address of a JobStreet or JobsDB results page. Its country, keywords, location and filters are used as they are.
4. Choose **What to return**: Jobs, Jobs and hiring companies, or Hiring companies only.
5. Optionally set filters: listed in the last N days, classification, work type, remote or hybrid, minimum monthly salary.
6. Click **Start**.

For a daily feed, go to **Schedules**, use the same input, and tick **Only new jobs since my last run**.

### Input example

```json
{
    "searches": [
        "accountant | Kuala Lumpur",
        "https://hk.jobsdb.com/accountant-jobs?daterange=7"
    ],
    "countries": ["my"],
    "output": "jobsAndCompanies",
    "findCompanyContacts": true,
    "skipRecruiters": true,
    "maxJobsPerSearch": 200,
    "datePosted": "7",
    "workTypes": ["fulltime"]
}
```

The first line searches Malaysia (the country picked). The pasted link searches Hong Kong, because a link always uses its own site.

### What data you get

The Output has four tabs: **Jobs**, **Salaries**, **Hiring companies** and **Run status**.

#### Job rows

| Field | What it is | Example |
| --- | --- | --- |
| `jobId`, `title`, `jobUrl` | Job ID, title and link | `95093670`, `https://my.jobstreet.com/job/95093670` |
| `companyName`, `advertiserName` | Company name, and the advertiser name the site shows (they can differ) | `3E Accounting` |
| `isRecruitmentAgency` | `true` if the advertiser looks like a recruitment or staffing agency | `false` |
| `isPrivateAdvertiser` | `true` when the ad hides the company ("Private Advertiser", "Company Confidential") | `false` |
| `location`, `city`, `region`, `country` | Where the job is. `city` is filled when the site gives a place and a region | `Jurong West, West Region`, `Jurong West`, `West Region`, `SG` |
| `workType`, `workArrangement` | Full time, part time, contract or casual. Remote/hybrid when the advertiser set it | `Full time`, `Remote` |
| `classification`, `subClassification`, `allClassifications` | The site's categories | `Accounting` |
| `salaryText` | Salary exactly as advertised | `RM 4,000 – RM 6,000 per month` |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | Parsed numbers, currency and period (`hour`, `day`, `week`, `month`, `year`) | `4000`, `6000`, `MYR`, `month` |
| `salaryMonthlyMin`, `salaryMonthlyMax` | Converted to a monthly figure | `4000`, `6000` |
| `salaryAnnualMin`, `salaryAnnualMax` | Converted to a yearly figure | `48000`, `72000` |
| `listedAt`, `daysAgo` | When it was listed | |
| `teaser`, `bulletPoints` | The short description and selling points from the search results | |
| `isPromoted`, `logoUrl` | Paid placement, and the advertiser logo | |
| `companyProfileUrl` | The company's profile page on the site, when it has one | |
| `site`, `search` | Which site and which of your search lines it came from | `my.jobstreet.com` |
| `isNew` | Only with "Only new jobs since my last run" | `true` |

With **Include full job descriptions** you also get `description` (the full ad as plain text), `phoneNumber` (if the advertiser listed one), `adEmails`, `adPhones` and `adWebsites` (contact details written in the ad), `applyType` (`quick apply` on the site or `external`), `isVerifiedAdvertiser` and `expiresAt`.

A bare `$` is read as Singapore dollars on sg.jobstreet.com, Hong Kong dollars on hk.jobsdb.com, and US dollars on the other four sites (offshore jobs in the Philippines often pay in USD).

Salary conversion uses 40 hours × 52 weeks for hourly rates, 260 working days for daily rates, 52 for weekly and 12 for monthly. A 13th-month bonus (common in Hong Kong, Singapore and the Philippines) isn't added, because most ads don't say whether there is one. When an ad doesn't state the period, it's worked out from the amount for that currency and `salaryPeriodGuessed` is `true`.

#### Hiring company rows

One row per company per country across all your searches (jobs from private advertisers can't be linked to a company, so they're left out).

| Field | What it is |
| --- | --- |
| `companyName`, `country` | The company and the country of its ads |
| `openJobs`, `jobTitles`, `locations`, `regions`, `classifications`, `workTypes` | What they're hiring for in this run's results |
| `newestListingDate`, `salaryMonthlyRange` | Their most recent ad and the range of advertised monthly salaries |
| `isRecruitmentAgency` | `true` if any of their ads look like an agency's, or their company profile lists them under Employment Services |
| `website`, `websiteSource` | Their website, and where it came from: their company profile, their company page, a web address in the job ad, the domain of an email in the ad, or (with contact finding on) an address made from the company name whose homepage shows that name |
| `industry`, `companySize`, `companyRating`, `companyReviewCount` | From their company profile on the site, when they have one |
| `emails`, `phones`, `socials`, `contactPageUrl` | With **Find company emails and phone numbers** on: found on their homepage and contact page, plus any in their job ads. The company's own-domain emails come first |
| `jobAdEmails`, `jobAdPhones` | The contact details that were written in their job ads (often the hiring manager or recruiter) |
| `contactStatus` | `found`, `no website found`, `none found on website`, `website unreachable (...)`, or `not requested` |
| `companyProfileUrl`, `jobUrls` | Links back to the site |

Example from a real run (search `accountant`, Singapore, 17 Oct 2026, trimmed):

```json
{
    "type": "company",
    "companyName": "Singapore LNG Corporation Pte Ltd",
    "website": "https://www.slng.com.sg/",
    "websiteSource": "company profile",
    "emails": ["enquiries@slngcorp.com"],
    "phones": ["+65 6376 7856", "+65 6273 7807"],
    "socials": { "facebook": "https://facebook.com/SLNGCorp", "linkedin": "https://linkedin.com/company/slng---singapore-lng-corporation" },
    "jobTitles": ["Human Resources Executive", "Analyst (Storage Analysis)"],
    "contactStatus": "found",
    "openJobs": 2,
    "industry": "Oil & Gas Extraction & Refinement",
    "companySize": "101-1,000 employees",
    "isRecruitmentAgency": false
}
```

#### Run status

One free row per search (and per country) saying what happened, for example: "Delivered 300 jobs. JobStreet Malaysia shows 1,931 for this search. Stopped at your limit of 300." Searches with no results, links that can't be used, and searches the site refused each get their own row, and you aren't charged for jobs from them. There's one more row for the hiring companies. A run summary is saved as `OUTPUT` in the key-value store.

### Pricing

You pay per result, nothing for empty searches or status rows.

| Event | Price |
| --- | --- |
| Job | $1.00 per 1,000 jobs |
| Full job description (only if you turn it on) | $0.50 per 1,000 jobs |
| Hiring company row | $0.002 per company |
| Contact details found for a company (only if you turn it on, and only when at least one email or phone number is found) | $0.008 per company |
| Run start | $0.00005 per run |

So a company lead with contact details costs $0.01, and one where nothing was found costs $0.002. For example:

| Run | Cost |
| --- | --- |
| 1,000 jobs | $1.00 |
| 1,000 jobs with full descriptions | $1.50 |
| 20 jobs with full descriptions, plus their 17 hiring companies, 9 with contacts (the Singapore test above) | about $0.14 |
| 100 hiring companies only, with contacts found for 70 | $0.76 |

If you set a maximum cost per run, the run stops cleanly at that limit and the status row says so.

### Good to know

- **Each search lists at most 500 jobs.** If you ask for more and haven't picked a classification, the search is split by classification automatically. If you have picked one, narrow the search with a location or date filter, or split it into several lines.
- **Locations** work like the search box on each site, in English: "Kuala Lumpur", "Jakarta Raya", "Metro Manila", "Bangkok", "Kwun Tong District". Leave it out to search the whole country.
- **Recruitment agency detection** uses the advertiser's name (agency brands and words like "Recruitment", "Staffing", "Manpower" and "Agensi Pekerjaan") and, for company rows, the industry on their company profile ("Employment Services"). The sites don't label agencies, so a few will slip through either way.
- **Only new jobs** remembers job IDs per search, country and filter combination, for about four months. Changing a search or its filters starts fresh. **Only companies I haven't had before** works the same way for companies.
- **Contact details** come only from public pages: the company's own website and the job ad. Nothing behind a login is accessed. Use them in line with the data protection and anti-spam laws of the country you're contacting (for example Malaysia's PDPA, Singapore's PDPA and Spam Control Act, Indonesia's PDP Law, the Philippines' Data Privacy Act, Hong Kong's PDPO and Thailand's PDPA).
- **Searching Australia or New Zealand?** Use [SEEK Jobs Scraper + Hiring Company Leads](https://apify.com/snappytools/seek-jobs-scraper). Same input, same output, same prices.
- This tool isn't affiliated with or endorsed by JobStreet, JobsDB or SEEK.

### Getting the data out

Download results as Excel, CSV or JSON from the Output tab, or send them on automatically: Google Sheets, Slack, Zapier, Make and webhooks are all in the **Integrations** tab, and every run is available through the Apify API.

### Questions or problems

Open an issue on the **Issues** tab and include the run link. I reply within one business day.

# Actor input Schema

## `searches` (type: `array`):

One search per line: keywords, then a | and a location. For example "accountant | Kuala Lumpur", "software engineer | Singapore", "sales | Jakarta Raya" or "nurse" for the whole country. You can also paste a link from a JobStreet or JobsDB results page; its country and filters are kept.

## `countries` (type: `array`):

Which sites to search. Each search line runs in every country you pick. A pasted link uses only its own country.

## `output` (type: `string`):

Jobs only, jobs plus one row per hiring company, or just the hiring companies (for lead lists).

## `findCompanyContacts` (type: `boolean`):

For each hiring company, find its website (from its company profile, the job ad, or its name) and check its homepage and contact page for emails, phone numbers (including WhatsApp links) and social links. Needs "Jobs and hiring companies" or "Hiring companies only". Slower: allow about 1 to 3 seconds per company.

## `skipRecruiters` (type: `boolean`):

Skip ads whose advertiser looks like a recruitment or staffing agency (Randstad, Michael Page, PERSOLKELLY, "... Manpower", "Agensi Pekerjaan ..." and so on), so you only get companies hiring for themselves. Based on the advertiser name, so a few may slip through.

## `maxJobsPerSearch` (type: `integer`):

The most jobs to collect for each search line. The site lists at most 500 jobs per search; above that the search is split by classification automatically.

## `datePosted` (type: `string`):

Only jobs listed in this many days.

## `classifications` (type: `array`):

Only jobs in these classifications. Leave empty for all.

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

Leave empty for all.

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

Leave empty for all. Many advertisers don't set this, so picking one leaves those jobs out.

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

Only jobs paying at least this much, in the country's own currency (RM, S$, Rp, ₱, HK$, ฿), using the site's own salary filter. Leave at 0 for no minimum.

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

Whether the minimum above is per month (the usual way salaries are shown on these sites), per year or per hour.

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

Newest first is best for schedules.

## `includeDescriptions` (type: `boolean`):

Adds the full ad text, the phone number if listed, emails and websites written in the ad, and whether applying goes to an external site. Costs a little extra per job.

## `onlyNewSinceLastRun` (type: `boolean`):

For schedules. Remembers the jobs each search has already returned and skips them next time. The first run returns everything, as usual.

## `newCompaniesOnly` (type: `boolean`):

Skip hiring companies that an earlier run already delivered (with the same searches, or the same watch name). Stops you paying twice for the same lead.

## `watchName` (type: `string`):

Optional. Runs with the same searches share their memory automatically. Give a name to keep two schedules separate, or to share one company memory across different searches.

## Actor input object example

```json
{
  "searches": [
    "accountant | Kuala Lumpur"
  ],
  "countries": [
    "my"
  ],
  "output": "jobs",
  "findCompanyContacts": false,
  "skipRecruiters": false,
  "maxJobsPerSearch": 20,
  "datePosted": "any",
  "classifications": [],
  "workTypes": [],
  "workArrangements": [],
  "salaryMin": 0,
  "salaryType": "monthly",
  "sortBy": "newest",
  "includeDescriptions": false,
  "onlyNewSinceLastRun": false,
  "newCompaniesOnly": false
}
```

# Actor output Schema

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

No description

## `salaries` (type: `string`):

No description

## `companies` (type: `string`):

No description

## `status` (type: `string`):

No description

## `allResults` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "searches": [
        "accountant | Kuala Lumpur"
    ],
    "countries": [
        "my"
    ],
    "output": "jobs",
    "maxJobsPerSearch": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("snappytools/jobstreet-jobsdb-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 = {
    "searches": ["accountant | Kuala Lumpur"],
    "countries": ["my"],
    "output": "jobs",
    "maxJobsPerSearch": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("snappytools/jobstreet-jobsdb-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 '{
  "searches": [
    "accountant | Kuala Lumpur"
  ],
  "countries": [
    "my"
  ],
  "output": "jobs",
  "maxJobsPerSearch": 20
}' |
apify call snappytools/jobstreet-jobsdb-scraper --silent --output-dataset

```

## MCP server setup

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