Watsons Thailand Price & Promotion Watch
Pricing
from $2.10 / 1,000 product scrapeds
Watsons Thailand Price & Promotion Watch
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.
Pricing
from $2.10 / 1,000 product scrapeds
Rating
0.0
(0)
Developer
Superslow Sloth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days ago
Last modified
Categories
Share
Scrape Watsons Thailand (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
Acceptheader: 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 forhair serum, flagged only byisNotExactMatchResult. 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
searchTermsempty and raisemaxItems. - De-duplication. Rows are de-duplicated by
product_codewithin 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.