# Watsons Thailand Price & Promotion Watch (`superslowsloth/watsons-th-products`) Actor

Search Watsons Thailand (beauty, skincare, health) by keyword or read the whole catalogue: one flat row per product with price in THB, regular price, discount, member and Elite prices, promotion tags with dates, multi-buy and flash-sale offers, stock, rating, brand, EAN, image and URL.

- **URL**: https://apify.com/superslowsloth/watsons-th-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 product scrapeds

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

## Watsons Thailand Price & Promotion Watch

Scrape [Watsons Thailand](https://www.watsons.co.th) (beauty, skincare, makeup,
health and personal care) by keyword, or read the **whole catalogue**, and get one
flat row per product: today's price in baht, the regular price, the discount,
the **member and Elite member prices**, the promotion tag with its **start and end
dates**, multi-buy ("2 pieces for 1,290") and flash-sale offers, stock status,
rating, brand, EAN barcode, category, shades or sizes, image and product URL.
Run it on a schedule and diff the datasets to get a **daily Thai beauty and
pharmacy price and promotion watch**.

No account, no API key, no login, no browser. One request returns 40 products.

### Input

| Field | Notes |
|---|---|
| `searchTerms` | Zero or more search terms, one per line (`serum`, `ยาสีฟัน`, `maybelline`). **Leave empty to read the whole catalogue** (about 12,500 products), up to `maxItems`. A product found by two terms is returned once, under the first. |
| `maxItems` | Products to read per search term, default 100, up to 20,000. 40 per page. |
| `sort` | `relevance` (default), `best-seller`, `top-rated`, `newest`, `brand`, `price-asc`, `price-desc`. |
| `language` | `th` (default) or `en`: names, promotion wording and the product link. Prices are baht either way. |
| `proxyConfiguration` | Apify proxy by default (see "How it fetches"). |

#### Search terms

Watsons matches loosely: `serum` finds 1,489 products, including masks and
shampoos that mention serum. Colons in a term are replaced with spaces (they are
Watsons' own filter syntax and would change the search).

**A term Watsons has no match for returns no rows.** Instead of an empty list,
Watsons silently answers with the results for a *similar* word (`yarn` returns
`york`, `the` returns `the ordinary`). The actor detects that, returns none of those
rows, and logs which word Watsons used instead. Check the log if a term you expected
to match came back empty; use a more specific word or the brand name.

#### About the sorts

The actor checks each sort: Watsons reports which one it applied, and a term
fails (at no charge) rather than return rows in an order that was not asked
for. This matters because Watsons accepts any sort name and quietly uses its
best-seller order for one it does not know. Price sorts rank by `price`
(today's non-member price).

### Output

Prices are numbers in Thai baht; `currency` is always `THB`.

| Field | Notes |
|---|---|
| `query` | The search term that first found this product (`""` when you gave none). |
| `product_code`, `url` | Watsons product code (`BP_270873`) and the absolute `https://www.watsons.co.th/th/...` link (`/en/` for English). |
| `name`, `brand`, `short_description` | As Watsons shows them. In the Thai catalogue `name` is sometimes the English brand title: Watsons holds one name per product. |
| `ean`, `size`, `country_of_origin` | Barcode, pack size as printed (`30 Ml`) and origin. **Null** where Watsons lists none (about 4% of products have no EAN). |
| `category_path`, `categories` | The customer-facing category path and Watsons' navigation categories. A few products carry a one-letter placeholder as `category_path`; it is passed through as sent. |
| `price` | What a customer pays today with no card: the markdown price when marked down, otherwise the regular price. |
| `regular_price`, `was_price` | The price before the markdown. `was_price` is `regular_price` only when it is above `price`, **null otherwise, never 0**. |
| `discount_percent`, `discount_amount` | Watsons' own discount percentage and baht saved, **null on a product that is not marked down**. |
| `member_price`, `elite_member_price` | Prices that need a Watsons club card / Elite membership. **They are not folded into `price`**, so a product with only a member price has `on_sale` false and a member price below `price`. |
| `on_sale` | True when `was_price` exists. |
| `promotion`, `promotion_title` | The promotion tag on the shelf (`ชิ้นที่ 2 ลด 80%`, `ราคาสมาชิก`, `ซื้อ 1 แถม 1 ...`) and its longer wording (`2 ชิ้น 1290`). Null when none. |
| `promotion_start`, `promotion_end` | When the promotion runs, ISO 8601 UTC. Watsons sends these in Bangkok time (UTC+7) and the actor converts them. |
| `multi_buy` | "Buy N for a set price": `quantity`, `total_price`, `average_price`, `base_price`, `saving`, `start_date`, `end_date`. Empty list when none. |
| `flash_sale` | A running flash sale: `price`, `discount_percent`, `start_date`, `end_date`, `remaining_quota`, `total_quota`. Null when none. |
| `stock_status`, `in_stock` | `inStock`, `lowStock` or `outOfStock` as the search API reports it (not per store), and a boolean that is false only for `outOfStock`. |
| `rating`, `review_count` | Star rating out of 5. Watsons sends 0 for "no reviews yet"; that is **null** here. `review_count` is read from the product's own variant record and only when it agrees with `rating`; Watsons' search does not carry it for most products, so expect null. |
| `sold_message` | Watsons' "sold N pieces" badge, on the few products that have one. |
| `variants` | Shades or sizes of a product that comes in several: `product_code`, `name`, `option`, `ean`, `price`, `sale_price`, `stock_status`, `image_url`. Empty for a single-variant product. |
| `image_url` | The product photo. |

#### Honest nulls

A field Watsons did not send is `null`, never `0` or `""`. The two traps worth
knowing: a rating of 0 is "unrated" (null), and `was_price`, `discount_percent` and
`discount_amount` are null on every product that is not actually marked down.

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.0021** per product row written (`product-scraped`), i.e. $2.10 per
  1,000 products.

### How it fetches

It calls the JSON API behind Watsons' own search page
(`api.watsons.co.th/api/v2/wtcth/products/search`, SAP Commerce), 40 products per
request, paging with `currentPage` (0-based). Measured 2026-10-14: HTTP 200 and
JSON from a residential address on every request, and from Apify datacenter
exits in a ten-request probe, no key or cookie needed, **when the request
carries a browser TLS fingerprint**. A plain
HTTP client (httpx, requests) is refused with HTTP 403 "Access Denied" from
Akamai in the same test, so the actor fetches with a Chrome-impersonating client.
A refusal (403, 429, 5xx) is treated as the address being blocked, and the actor
rotates to a fresh one and retries. An HTTP 400 is permanent and is not retried.

Things worth knowing, all measured:

- **Prices up to 15 minutes old.** Watsons' edge caches each search URL for 15
  minutes, so a price changed in the last quarter hour may not show yet.
- **Cached XML.** The API's default content type is XML, and the edge cache
  ignores the `Accept` header: if anyone fetched a URL first without asking for
  JSON, everyone gets XML for 15 minutes. The actor detects that and retries the
  page from a changed URL (an extra throwaway parameter that does not alter the
  data).
- **Past the last page Watsons answers a different search.** Page 38 of `serum`
  (37 is the last) is a full page of results for `hair serum`, flagged only by
  `isNotExactMatchResult`. The actor reads the page count from the response and
  never asks for a page past it.
- **No depth limit.** The whole catalogue (313 pages) can be read in one run:
  leave `searchTerms` empty and raise `maxItems`.
- **De-duplication.** Rows are de-duplicated by `product_code` within a run, so
  a product that moves between pages while you read is returned once.
- **Not offered.** Per-store stock and prices, prescription medicines (none of
  the 1,356 products sampled was flagged as one), and filters by category or brand other than searching the
  brand name.

# Actor input Schema

## `searchTerms` (type: `array`):

What you would type into the Watsons Thailand search box, one term per line (for example serum, ยาสีฟัน or maybelline). Each term is searched and read separately. A product that two terms both find is returned once, under the first. Leave the list empty to read the whole catalogue (about 12,500 products) in Watsons' best-seller order, up to the limit below. A term Watsons has no match for is skipped and logged: Watsons would otherwise answer it with the results for a different word.

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

Read at most this many products for each search term (or for the whole catalogue when no term is given). Watsons serves 40 per page; a 100-product run reads 3 pages. Up to 20,000 are accepted, which is more than the whole catalogue.

## `sort` (type: `string`):

The order Watsons lists the results in. 'Relevance' is its default for a search term. The actor checks that Watsons applied the sort you chose and fails the term, at no charge, rather than return rows in another order.

## `language` (type: `string`):

Language of product names, promotion wording and the product link. Prices are in Thai baht either way. Thai is Watsons' default; English names come from the same catalogue.

## `proxyConfiguration` (type: `object`):

Apify proxy (automatic datacenter exits) is the default. A refused request (403, 429) is retried from a fresh address, which is what the proxy is for. Watsons' edge refuses plain HTTP clients but answered Apify datacenter exits when fetched with a browser-like TLS fingerprint, which this actor uses.

## Actor input object example

```json
{
  "searchTerms": [
    "serum"
  ],
  "maxItems": 100,
  "sort": "relevance",
  "language": "th",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "searchTerms": [
        "serum"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/watsons-th-products").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 = {
    "searchTerms": ["serum"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/watsons-th-products").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 '{
  "searchTerms": [
    "serum"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/watsons-th-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/watsons-th-products"
        }
    }
}
```

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/YEr0kLA2cxlccoX9f/builds/kkYVF7FcdZEglImIb/openapi.json
