# JobThai Scraper - Thailand Job Postings (`skusol/thailand-job-postings-scraper`) Actor

Scrape job postings from JobThai and get each one as JSON with salary min/max in THB, pay period, seniority, required years of experience, skills and remote status parsed by AI. Handles Thai-language postings. First 10 items enriched free.

- **URL**: https://apify.com/skusol/thailand-job-postings-scraper.md
- **Developed by:** [Kusol Sukhakul](https://apify.com/skusol) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 item extracteds

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?

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

## Thailand Job Postings Scraper

Scrapes Thai job postings from JobThai and returns each one as structured JSON,
with salary range, seniority, required experience, skills and remote status
normalized by an AI step into fields you can filter and aggregate.

### Try it free, no setup

Run it with the defaults. The first 10 postings come back with the full `ai`
block filled in, so you can see exactly what the AI step produces before
deciding whether it is worth anything to you. Postings past the tenth are
still returned in full — only the `ai` block is left empty, with `aiError`
saying why.

To enrich every posting in a run, put your own `talariaToken` in the input.
There is no signup wall on the demo and no card required to see the output.

### Sample output

One item, exactly as it lands in the dataset:

```json
{
  "url": "https://www.jobthai.com/th/company/job/1936578",
  "source": "jobthai",
  "scrapedAt": "2026-09-05T08:44:49+00:00",
  "title": "SALE & RECEPTION",
  "company": "ANY1 Fitness & Sports (บริษัท ธนสุวรรณ กรุ๊ป จำกัด)",
  "location": "บางแค, กรุงเทพมหานคร, TH",
  "employmentType": "FULL_TIME",
  "salaryText": "11000-15000 THB MONTH",
  "postedAt": "2026-09-05T08:41:54.000Z",
  "description": "รายละเอียดงาน / หน้าที่รับผิดชอบ:- ต้อนรับ ให้ข้อมูลเกี่ยวกับบริการ สิทธิประโยชน์ และแพ็กเกจสมาชิกแก่ลูกค้าที่เข้ามาใช้บริการ- นำเสนอขายแพ็กเกจสมาชิก/คอร์สออกกำลังกายให้บรรลุเป้าหมายยอดขาย …",
  "jobId": "1936578",
  "ai": {
    "salary_min_thb": 11000,
    "salary_max_thb": 15000,
    "salary_period": "monthly",
    "seniority": "entry",
    "experience_years_min": null,
    "skills": [
      "การต้อนรับ",
      "การนำเสนอขาย",
      "การดูแลงานเอกสาร",
      "การลงทะเบียนสมาชิก",
      "การรับชำระเงิน",
      "การติดตามลูกค้า",
      "การประสานงาน",
      "การส่งรายงานยอดขาย"
    ],
    "remote_status": "onsite"
  }
}
```

Everything outside `ai` is read from the posting. Everything inside `ai` is
inferred from the posting text by the AI step. When enrichment is turned off,
fails, or times out, `ai` is `null`, an `aiError` field says why, and the
posting is returned anyway.

> Note: the whole object above, `ai` included, is verbatim from a live
> `make actor-run` against jobthai.com and the deployed enrichment service on
> 2026-09-05, with the description truncated for length. Of the 3 items that
> run scraped, this one enriched cleanly; one of the other two hit a
> transient `503` from the enrichment service and came back with `ai: null`
> and an `aiError`, per the "return the raw item uncharged for AI" rule
> below.

### Input

| Option | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | JobThai job listing page | Listing or job pages to start from. Listing pages are followed to the jobs they link to. |
| `maxItems` | integer | 100 | Hard cap on items returned. Never exceeded. Maximum 1000. |
| `enrich` | boolean | `true` | Turns the paid AI step on or off. |
| `enrichFields` | array | all fields | Restricts enrichment to named fields: `salary_min_thb`, `salary_max_thb`, `salary_period`, `seniority`, `experience_years_min`, `skills`, `remote_status`. |
| `talariaToken` | string (secret) | none | Bearer token for the enrichment service. Required when `enrich` is on. |
| `talariaBaseUrl` | string | `https://talaria.skusol.com` | Point the AI step at your own endpoint instead. |
| `requestIntervalSecs` | integer | 1 | Minimum seconds between two requests to the target site. |
| `maxRetries` | integer | 3 | Retries per page before it is skipped. |
| `respectRobotsTxt` | boolean | `true` | Stops the crawl if robots.txt disallows it. |

### Use cases

1. **Salary benchmarking.** Postings state salary as free text, in several
   formats and two languages. `salary_min_thb`, `salary_max_thb` and
   `salary_period` give you numbers you can average per role and per province.
2. **Skill demand tracking.** Run the same query weekly and count `skills`
   across items to see which tools employers are actually asking for, rather
   than which ones a survey says they want.
3. **Recruitment lead lists.** Filter by `seniority`, `remote_status` and
   `experience_years_min` to find the companies hiring for the roles you place,
   with a link back to each posting.

### Pricing

Prices are set on the Apify Store listing; this table says what triggers each
event.

| Event | Charged when |
|---|---|
| `apify-actor-start` | Once, when a run starts. |
| `item-scraped` | Once per job posting after it has been written to the dataset. A posting that fails to parse is never pushed and never charged. |
| `ai-enriched-item` | Once per posting whose AI fields came back valid. Enrichment that fails, times out or is turned off is not charged. |

Two rules the actor holds to: nothing is charged before the thing it pays for
exists, and when a run reaches its maximum cost the run ends cleanly with
everything produced so far.

### Limitations

- **Rate.** With enrichment on, throughput is bounded by the AI service, which
  handles roughly six items per minute. A 100-item run with enrichment takes
  about 20 minutes. With `enrich: false` the limit is the polite crawl delay,
  one page per second by default.
- **Coverage.** JobThai only. Listing pages are followed one level to the job
  pages they link to; pagination beyond the pages you pass in `startUrls` is
  not crawled. Postings missing a title or description are skipped rather than
  returned half-empty.
- **Freshness.** Each item is a snapshot at `scrapedAt`. There is no change
  detection or deduplication across runs; a posting scraped twice appears
  twice.
- **AI fields are inferred.** `ai` values come from a language model reading
  the posting. A field the posting does not state comes back `null` or
  `unknown` rather than a guess, but the values are not verified against any
  other source. `salaryText` keeps the original wording so you can check.
- **Personal data.** Only posting content is collected: role, employer,
  location, salary, requirements. Contact people named on a posting are not
  extracted.
- **robots.txt.** With `respectRobotsTxt` on, a disallow ends the crawl instead
  of working around it.

### Development

```
make actor-setup   # once, needs the network
make build         # go build/vet/test, then compileall and pytest for the actor
make actor-run     # local run in pay-per-event test mode
```

The test suite runs offline. It uses fake pages, a fake enrichment service, and
the Apify SDK's local pay-per-event mode, and asserts on charge counts and
ordering.

# Actor input Schema

## `startUrls` (type: `array`):

Listing or job-detail pages to start from. Listing pages are followed to the job pages they link to; job pages are scraped directly.

## `maxItems` (type: `integer`):

Hard cap on how many job postings this run returns. Never exceeded.

## `enrich` (type: `boolean`):

Turn the paid AI step on or off. With it off, you get the scraped fields only and are never charged for enrichment.

## `enrichFields` (type: `array`):

Limit enrichment to these fields. Leave empty for all of them: salary\_min\_thb, salary\_max\_thb, salary\_period, seniority, experience\_years\_min, skills, remote\_status.

## `talariaToken` (type: `string`):

Bearer token for the enrichment service. Leave this empty to try the free demo: the first 10 items of the run come back with the full AI block, the rest are returned without it. Supply a token to enrich every item.

## `talariaBaseUrl` (type: `string`):

Override only if you run your own enrichment endpoint.

## `requestIntervalSecs` (type: `integer`):

Minimum seconds between two requests to the target site.

## `maxRetries` (type: `integer`):

How many times a failed page fetch is retried before it is skipped.

## `respectRobotsTxt` (type: `boolean`):

Stop the crawl if the target's robots.txt disallows it. Leave on.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.jobthai.com/th/jobs"
    }
  ],
  "maxItems": 100,
  "enrich": true,
  "enrichFields": [],
  "talariaBaseUrl": "https://talaria.skusol.com",
  "requestIntervalSecs": 1,
  "maxRetries": 3,
  "respectRobotsTxt": true
}
```

# Actor output Schema

## `jobPostings` (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 = {
    "startUrls": [
        {
            "url": "https://www.jobthai.com/th/jobs"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("skusol/thailand-job-postings-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 = { "startUrls": [{ "url": "https://www.jobthai.com/th/jobs" }] }

# Run the Actor and wait for it to finish
run = client.actor("skusol/thailand-job-postings-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 '{
  "startUrls": [
    {
      "url": "https://www.jobthai.com/th/jobs"
    }
  ]
}' |
apify call skusol/thailand-job-postings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,skusol/thailand-job-postings-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/yqGh3Jo2xQOAdedHo/builds/b09aVSZp1jvKEcAf0/openapi.json
