# Lazada Scraper - Products, Prices, Sellers & Reviews (`abotapi/lazada-scraper`) Actor

Scrape Lazada products and reviews across SEA markets, including Malaysia, Singapore, Indonesia, Philippines, Thailand, and Vietnam. Extract structured data on pricing, inventory, ratings, sellers, media, and reviews. Ideal for cross-region analysis with consistent, analytics-ready output.

- **URL**: https://apify.com/abotapi/lazada-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 153 total users, 30 monthly users, 79.7% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Lazada Scraper: Products, SKUs, Vouchers & Reviews Across 6 SE Asia Sites

Lazada Scraper turns Lazada into structured e-commerce data across all six Southeast Asia storefronts: Singapore, Malaysia, Thailand, Vietnam, the Philippines and Indonesia. Search by keyword or feed it search-result, category and product URLs to pull product listings with full SKU variant tables, vouchers, shipping and warranty terms, seller ratings, and customer reviews with photos and videos. Flip one switch to collect reviews only, walked page by page instead of just the first page. Export to JSON, CSV or Excel, or read results straight from the dataset through the API.

### Why This Scraper?

- **Six Southeast Asia storefronts in one actor.** One country field switches searches between Singapore, Malaysia, Thailand, Vietnam, the Philippines and Indonesia, and in URL mode (including reviews-only runs on product URLs) each URL's own host picks its storefront, so one run can mix them.
- **Two ways to find products.** Run a keyword search with price, rating and free shipping filters, or feed it search result, category or product URLs directly.
- **Full product depth on demand.** Turn on detail page enrichment for the complete SKU variant table (price, stock and attributes per variant), every active voucher and promotion, shipping fee and delivery window, warranty period, and the seller rating cluster.
- **Reviews at real depth.** Reviews only mode walks every review page for a product, capped by a limit you set, not just the first page, and each review carries its rating, content, buyer handle and any photo or video media.
- **Real specials, never invented.** `isOnSpecial`, `discountAmount`, `promoLabel` and `specialsCategory` key off Lazada's own strike-through price and badges, or its real Flash Sale campaign feed, never a synthesized markdown.
- **Built for monitoring.** Resume one interrupted run without re-collecting what it already found, or turn on Incremental mode to schedule the same search and get only new, updated and expired products on every recurring run.
- **Every record keeps the source.** Flat top-level fields sit alongside a nested `raw` object holding Lazada's own listing and detail-page payload, so downstream pipelines never lose a field.

### Use Cases

- **Price and catalogue monitoring:** track prices, stock and voucher changes across SG, MY, TH, VN, PH and ID with scheduled, incremental runs.
- **Marketplace research and competitor tracking:** pull seller ratings, item sold counts and SKU-level pricing to benchmark sellers and brands.
- **Review mining and sentiment analysis:** collect every review, with photos and videos, for a product line to spot quality issues or standout features.
- **Deal and Flash Sale tracking:** walk the real Flash Sale feed for strike-through prices, discount amounts, and LazMall or Campaign badges.
- **Catalogue and content enrichment:** feed product titles, images, descriptions and specifications into a PIM, comparison site or shopping assistant.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

#### Product record (`type: "product"`)

| Field | Example |
| --- | --- |
| `type` | `"product"` (also `"review"`) |
| `id` / `productId` | `"0000000001"` |
| `url` / `productUrl` | `"https://www.lazada.sg/products/sample-i0000000001.html"` |
| `productName` | `"Sample Product Title"` |
| `currentPrice` / `originalPrice` / `discountPct` | `49.9` / `99` / `50` |
| `isOnSpecial` / `discountAmount` | `true` / `49.1` |
| `promoLabel` / `specialsCategory` | `"Flash Sale"` / `"flash-sale"` |
| `currency` / `country` | `"SGD"` / `"SG"` |
| `ratingScore` / `reviewCount` | `4.6` / `1234` |
| `itemSold` | `"5K+ sold"` |
| `inStock` / `isSponsored` / `freeShipping` | `true` / `false` / `true` |
| `sellerName` / `sellerId` | `"Sample Store"` / `"0000000"` |
| `sellerLocation` | `"Singapore"` (from the listing card on search and category rows; empty when a product URL is opened directly). Lazada Vietnam search pages give the country (`Vietnam`) unless **Use the storefront's own language** is on, which returns the province or city |
| `brandName` / `brandId` | `"Sample Brand"` / `"0000"` (from the listing card on search and category rows) |
| `primaryImage` | `"https://laz-img-cdn.alicdn.com/p/000000.jpg"` |
| `skuCount` | `4` (detail page fetch only) |
| `seedType` / `seedValue` | `"query"` / `"wireless earbuds"` |
| `sourceUrl` | the search or URL start that discovered this product |
| `fetchedAt` | `"2026-01-01T00:00:00.000Z"` |

When **Enrich each product with detail page data** is on, product records also carry `imageGallery`, `description`, `descriptionHtml`, the full `skuList` (variant id, price, stock, attributes), `skuProperties` (the variant axes such as Color or Size, each with its values and which one is selected), `minQuantity` and `maxQuantity` (purchase limits), `highlights` (the product's bullet points), `vouchers`, `promotions`, `shipping` (fee, ETA, COD flag), `warranty`, `specifications` and `breadcrumbs`, plus `raw.serp` and `raw.detail` holding the complete source objects. The seller rating cluster fields `sellerRatingPositivePct`, `sellerShippingSpeed`, `sellerChatResponseRate` and `sellerFollowerCount`, plus `skuCount`, also populate with the detail page fetch. `brandName`, `brandId` and `sellerLocation` come from the listing card on search and category rows (so they are filled without detail enrichment); a product page opened directly by URL does not publish a seller location, so `sellerLocation` stays empty there. When **Include reviews on each product** is on, each product additionally carries an `embeddedReviews` array of its top reviews.

Every product record also carries `skuId`, `language` (the storefront language code), `canonicalUrl` (product URL without tracking parameters), `stockStatus` (`in_stock` or `out_of_stock`, when the listing says so), `badges` (the listing's readable badge labels, such as lazMall or a coin-saving banner; icons without a text label are skipped) and `enrichmentStatus` (`listing-only`, `enriched` or `detail-unavailable`). With detail enrichment on you also get `categoryPath` (same breadcrumbs as `breadcrumbs`) and `descriptionImageUrls` (images inside the description HTML). Review records add `isMeLike` and `videoUrls` when the review has a video.

Real specials, never invented: `isOnSpecial`, `originalPrice` and `discountAmount` key off Lazada's own strike-through price, populated only when it genuinely exceeds `currentPrice`. A full-price item gets `isOnSpecial: false` and no `originalPrice` or `discountAmount` field. `specialsCategory` and `promoLabel` surface Lazada's own classification, `flash-sale` when the record came from the Flash Sale campaign feed, `lazmall` or `campaign` when the card carries that exact badge, or empty when neither applies. Generic perks such as free shipping or coin and voucher cashback are not specials and are never mapped to these fields.

Review records (`type: "review"`, emitted when **Output reviews only** is on) carry `reviewId`, `productId`, `productUrl`, `buyerName`, `rating`, `reviewTime`, `reviewContent`, `skuInfo`, `likeCount` (the review's helpful votes), `media` (photo and video attachments), `country`, `raw` and `fetchedAt`. The seller's reply to a review, when there is one, is kept in `raw.reply` rather than as its own column. Review text comes back in whatever language the buyer wrote it in, Thai on lazada.co.th, Vietnamese on lazada.vn, Bahasa Indonesia on lazada.co.id, and so on, since it is never translated.

#### Output views

The Output tab offers these views of the dataset: **Results** (product listing fields), **All records** (every field), **Reviews** (review records), **Product detail** (SKU table, vouchers, shipping, warranty, seller cluster), **Changes (incremental)** (change status and `changedFields`) and **Product reviews (fetchReviews)** (the embedded reviews of each product). A run summary is also saved under the OUTPUT key; in a reviews-only run it lists `productsSkipped` (product ids whose reviews could not be read on any connection) when there were any.

#### Pricing

You pay per record returned. The **Pricing** tab on the actor page shows the exact rates, including the per-run start charge and the optional detail-enrichment surcharge. Reviews fetched with **Include reviews on each product** are billed on the same detail-enrichment event in blocks of 10: the unit that covers a product page read also covers its first 10 reviews, then one more unit per started block of 10 reviews actually returned on the row (a product with 25 reviews is 3 units; without a product page read it is 3 units for the reviews alone). Nothing is billed for a row that is not returned. In **Output reviews only** mode each review record is a returned record and is billed as one, with no block charge. Unchanged and expired products in incremental mode are only returned and billed when you turn on **Emit unchanged** or **Emit expired**.

### How to Use

1. Pick a **mode**: `search` (run one or more keyword queries) or `url` (walk pasted search, category or product URLs).
2. Pick the **country** site (`sg`, `my`, `th`, `vn`, `ph` or `id`). In URL mode every URL's own host decides its country site, so one run can mix `lazada.sg` and `lazada.com.my` URLs.
3. Set filters and enrichment: price range, minimum rating, free shipping, Flash Sale only, and whether to fetch detail pages and reviews.
4. Set **Max products total** to control run size and cost, then click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by query:**

```json
{
  "mode": "search",
  "country": "sg",
  "queries": ["wireless earbuds"],
  "minPrice": 20,
  "maxPrice": 200,
  "minRating": 4,
  "sortBy": "priceDesc",
  "maxPages": 5,
  "maxListings": 100,
  "fetchDetails": true
}
```

**Walk search result URLs:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.lazada.com.my/catalog/?q=ipad",
    "https://www.lazada.com.my/shop-laptops/"
  ],
  "maxPages": 3,
  "maxListings": 60,
  "fetchDetails": true
}
```

**Reviews only, from product URLs:**

```json
{
  "mode": "url",
  "country": "sg",
  "urls": [
    "https://www.lazada.sg/products/sample-i0000000001.html",
    "https://www.lazada.sg/products/sample-i0000000002.html"
  ],
  "reviewsOnly": true,
  "maxReviewsPerProduct": 200
}
```

**Products with embedded reviews:**

```json
{
  "mode": "search",
  "country": "ph",
  "queries": ["coffee maker"],
  "fetchReviews": true,
  "maxReviewsPerProduct": 30,
  "maxPages": 2,
  "maxListings": 40
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/lazada-scraper").call(run_input={"mode": "search", "country": "sg", "queries": ["wireless earbuds"]})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["productName"], item["currentPrice"], item["currency"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/lazada-scraper').call({ mode: 'search', country: 'sg', queries: ['wireless earbuds'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### URL mode and supported URL shapes

URL mode accepts the shapes below. Each URL's country site is detected from its own host. **Behaviour change:** the `urls` field now ships a default holding one example of each shape, so a URL-mode run with the field untouched scrapes one example of each shape (capped by Max products total) instead of nothing. Likewise `queries` now defaults to `laptop`, so a run that omits both fields scrapes the `laptop` search instead of finishing with 0 items. A run where no query or URL is usable now ends FAILED with a clear message.

| Shape | Example |
|---|---|
| Keyword search | `https://www.lazada.sg/catalog/?q=earbuds` |
| Filtered, sorted, paginated search | `https://www.lazada.sg/catalog/?q=iphone&price=100-500&sort=priceasc&page=2` |
| Category page | `https://www.lazada.sg/shop-laptops/` |
| Paginated category | `https://www.lazada.sg/shop-laptops/?page=2` |
| Flash Sale feed | `https://www.lazada.sg/tag/flash-sale/` |
| Tag landing page | `https://www.lazada.sg/tag/earbuds/?q=earbuds&catalog_redirect_tag=true` |
| Product detail | `https://www.lazada.sg/products/la-gourmet-55l-crystalline-air-fryer-i3646864772.html` |
| Other country site | `https://www.lazada.com.my/catalog/?q=kopi` |

A product URL is matched on its numeric item id, so the slug part is irrelevant (an id-only `/products/pdp-i<id>.html` link works too). A store homepage such as `https://www.lazada.sg/` is skipped with a warning because it has no product listing of its own; it is not in the example list. Max products total is shared across all URLs.

#### Flash Sale and full catalogue walks

Set **Flash Sale only** (`specialsOnly`) in search mode to walk Lazada's real Flash Sale campaign feed instead of the plain catalog, a genuinely different, curated product set rather than a client-side filter. Leave **Search queries** empty to browse the whole feed, or add queries to search within it. Pasting a Flash Sale URL in url mode reaches the same feed with no extra input. LazMall and Campaign badges are detected on any card regardless of `specialsOnly`, so a plain keyword search can already return `specialsCategory: "lazmall"` for LazMall-badged listings.

**Max SERP pages per query/URL** (`maxPages`) has a hard cap of 100 pages per query or URL: leave it empty or at `0` to walk up to that cap, stopped earlier by an empty page, Lazada's own end-of-results signal, and **Max products total** (`maxListings`).

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run, or splits a very large full-catalogue walk across several runs: paste a prior run or dataset id and the actor skips every product id already collected there, so you do not pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduling the same search or URL configuration again and again. The remembered state is kept in a named key-value storage in your account called `lazada-com-incremental-state`, so it survives between runs; delete it to start every search from scratch. Each product is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED`, or `EXPIRED` (only after a run that scanned the full tracked scope without a cap, and only with `emitExpired` on; a pasted URL that starts at page 2 or later is a partial scope and never produces them). What is compared: every output field of the product record (price, discount, stock, rating, review count, sold count, seller, images, SKU list, vouchers and so on). What is excluded: the per-request coupon request parameters inside each SKU (`skuList.price.coupon.requestParameters`, they carry a fresh random id on every read), inside embedded reviews the signed video links, the review date text (relative "4 weeks ago" becomes an absolute date later) and the `raw` copy, `fetchedAt` and the `raw` card passthrough always, plus anything you list in `ignoreFieldsForChanges` (dotted paths such as `skuList.stock` reach inside lists; ignored fields stay in the output and are only left out of the comparison and `changedFields`). Changing the ignore list never causes a one-time wave of UPDATED rows. Fields added in newer versions of the actor (for example `skuId`, `badges`, `categoryPath`, `skuProperties`) are compared like every other field; a product stored by an older version simply does not hold them yet, so for that one product on the first run after the upgrade they are left out of the comparison and then stored, and a real change to them is reported from the next run on. `stateKey` names or shares the stored state; leave it empty and the actor derives one automatically from the search and filter configuration. With incremental mode off, output is exactly as before. Reviews only runs never produce `EXPIRED` rows, since there is no discoverable product universe to diff against. Combining Resume with Incremental mode on a search that already has saved incremental state fails the run immediately, to avoid mixing two different baselines; combining them on a brand new search bootstraps the incremental baseline from the resumed dataset instead. When a product page cannot be read in a run (`enrichmentStatus: "detail-unavailable"`) but was read before, the earlier detail fields are kept for the comparison, so an unreadable page is never reported as a product change.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape, the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items 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 |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` or `url`. |
| `country` | string | `sg` | One of `sg`, `my`, `th`, `vn`, `ph`, `id`. In search mode it is the one site searched. In URL mode each URL's own host decides its site (and its connection exit); this value is only the fallback for URLs on an unknown host. |
| `queries` | array | none (prefilled `["laptop"]`) | Search terms, one paginated walk per query. Search mode only. |
| `categoryId` | string | none | Deprecated and hidden. Lazada has no numeric category id search facet; use `mode: "url"` with a pasted category page URL instead. Kept for backward compatibility with saved tasks and has no effect. |
| `minPrice` | integer | none (no lower bound) | Minimum price in the country's local currency. |
| `maxPrice` | integer | none (no upper bound) | Maximum price in the country's local currency. |
| `specialsOnly` | boolean | `false` | Walk Lazada's real Flash Sale feed instead of the plain catalog (search mode). |
| `sortBy` | string | `popularity` | `popularity`, `priceAsc` or `priceDesc`, the only three sort options Lazada's own search page exposes. `ratingDesc`, `newest` and `bestSelling` are still accepted for saved tasks but run as `popularity`. |
| `urls` | array | one example of each supported URL shape (see the URL shapes table) | Search result, category, tag, Flash Sale or product URLs. URL mode only. |
| `minRating` | integer | none | Drop products whose average rating is below this value (1 to 5), applied after the search fetch. |
| `freeShippingOnly` | boolean | `false` | Keep only items carrying Lazada's own Free Shipping badge, in both search and URL mode. |
| `localLanguage` | boolean | `false` | Read search pages in the storefront's own language, in both search and URL mode. On Lazada Vietnam `sellerLocation` becomes the province or city (for example `Hà Nội`) instead of `Vietnam`, with Vietnamese titles; on Lazada Thailand titles and the city name switch to Thai script. Other sites are unchanged. Switching it starts a new Incremental baseline. |
| `detailMode` | string | `full` | Deprecated and hidden. Ignored: product pages are always read in full when `fetchDetails` is on. Kept so saved tasks that still send it keep working. |
| `reviewsOnly` | boolean | `false` | Emit one record per review (`type: "review"`) instead of one per product. |
| `fetchReviews` | boolean | `false` | Add an `embeddedReviews` array to each product record (only when `reviewsOnly` is off). In Incremental mode only the newest 5 embedded reviews of a product are compared for changes. |
| `maxReviewsPerProduct` | integer | `20` | Hard cap on reviews fetched per product. The default was lowered from 100 to 20 to keep default runs cheap; raise it (up to 5000) for deeper review coverage. |
| `fetchDetails` | boolean | `false` | Fetch each product's detail page for the SKU table, vouchers, shipping, warranty, seller cluster and images. |
| `maxPages` | integer | none (prefilled `0`, meaning unlimited) | Bound on result pages walked per starting query or URL. `0` walks up to the hard cap of 100 pages, still bounded by `maxListings`. |
| `maxListings` | integer | `20` | Hard cap on products across all queries or URLs. `0` means unlimited (still bounded by `maxPages`). In reviews-only mode this caps discovered products, not review records. |
| `resumeFromRunId` | string | none | Paste a previous run or dataset id to continue a walk without returning or charging for products already collected there. |
| `incrementalMode` | boolean | `false` | Turn on for scheduled runs to classify each product against the previous run's state. |
| `stateKey` | string | none | Name or share an incremental-mode state; left empty, one is derived automatically from the search and filter configuration. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) products unchanged since the last incremental run. |
| `emitExpired` | boolean | `false` | Also return (and bill) products no longer found after a complete incremental scan. |
| `ignoreFieldsForChanges` | string\[] | `[]` | Ignore fields when detecting changes (incremental mode only): field names (dotted paths allowed) that never make a product UPDATED. |
| `proxy` | object | none (prefilled to Apify Residential) | Connection settings. Residential is tried first, see Plan Requirement. The exit country always follows the Lazada site being fetched, in every mode: a `lazada.com.my` URL, or search with country `my`, is fetched through a Malaysian exit even when the prefill says `SG`. Your own proxy URLs are used as given. |
| `mcpConnectors` | array | none | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "type": "product",
  "id": "0000000001",
  "url": "https://www.lazada.sg/products/sample-i0000000001.html",
  "productName": "Sample Wireless Earbuds",
  "productId": "0000000001",
  "productUrl": "https://www.lazada.sg/products/sample-i0000000001.html",
  "sourceUrl": "https://www.lazada.sg/catalog/?q=wireless+earbuds",
  "seedType": "query",
  "seedValue": "wireless earbuds",
  "country": "SG",
  "currency": "SGD",
  "currentPrice": 49.9,
  "originalPrice": 99,
  "discountPct": 50,
  "isOnSpecial": true,
  "discountAmount": 49.1,
  "promoLabel": "Flash Sale",
  "specialsCategory": "flash-sale",
  "ratingScore": 4.6,
  "reviewCount": 1234,
  "itemSold": "5K+ sold",
  "inStock": true,
  "isSponsored": false,
  "freeShipping": true,
  "sellerName": "Sample Store",
  "sellerId": "0000000",
  "sellerLocation": "Singapore",
  "sellerRatingPositivePct": 98,
  "sellerShippingSpeed": "Fast",
  "sellerChatResponseRate": "95%",
  "sellerFollowerCount": 1500,
  "brandName": "Sample Brand",
  "brandId": "0000",
  "primaryImage": "https://laz-img-cdn.alicdn.com/p/000000.jpg",
  "imageGallery": ["https://laz-img-cdn.alicdn.com/p/000001.jpg"],
  "skuList": [{ "id": "0000", "price": 49.9, "stock": 10, "color": "Black" }],
  "skuCount": 4,
  "vouchers": [{ "amount": 5, "minSpend": 30, "code": "SAMPLE5" }],
  "shipping": { "fee": 0, "eta": "2-3 days", "cod": true },
  "warranty": { "type": "Manufacturer", "periodMonths": 12 },
  "breadcrumbs": ["Electronics", "Audio", "Earphones"],
  "raw": { "serp": {}, "detail": {} },
  "fetchedAt": "2026-01-01T00:00:00.000Z"
}
```

### Plan Requirement

Lazada's product listings and reviews load through a protected data route that challenges non-residential connections (measured: a datacenter connection gets a slider challenge on that route, a residential connection pinned to the storefront country returns results). Plain page routes such as category and product pages may load on other connection types, but the listing and review data this actor reads does not, so a paid Apify plan with Residential proxy access is recommended and is what the actor tries first, with its exit in the country of the Lazada site being fetched (the country pin in the Proxy field is overridden to match; your own proxy URLs are used as given, so make sure their country matches). If you omit the **Proxy** field the actor sets this up for you.

On Apify free plans, which do not include residential proxies, the actor logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." and continues on a backup connection, logging "Served over the backup connection (the default connection was unavailable)." when it is used. If neither connection can load a page the run ends FAILED with one of these messages:

- "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid."
- "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Lazada pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
- "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

When one run mixes storefronts (for example `lazada.sg` and `lazada.com.my` URLs) and one of them cannot be loaded, the actor logs "Storefront MY: no results page could be loaded for 1 start(s); continuing with the other storefronts.", keeps and bills the rows the other storefronts returned, and skips the expired-product check for that run. The run ends FAILED with one of the messages above only when no storefront returned a page.

If you explicitly choose a non-residential proxy group, the actor keeps your choice as its first connection. On a retry, the actor draws a fresh connection within the same target country before falling back to a neighboring Southeast Asia exit.

### FAQ

#### How much does it cost?

You pay per record returned, with the detail-enrichment event added when **Enrich each product with detail page data** is turned on, and in blocks of 10 reviews when **Include reviews on each product** is turned on (see Pricing above). The **Pricing** tab on the actor page shows the current rates. Use **Max products total** to cap the cost of any run.

#### Does it support Lazada Vietnam (lazada.vn)?

Yes. Vietnam is one of the six storefronts (`country: "vn"`, prices in VND). Product records carry `currentPrice`, `originalPrice`, `discountPct`, `sellerName`, `sellerId`, `sellerLocation`, `ratingScore`, `reviewCount` and `itemSold`; turn on **Enrich each product with detail page data** for the SKU table, shipping and warranty terms, and **Include reviews on each product** for `embeddedReviews` (rating, review text, buyer handle, variant and photos). By default Lazada's Vietnam search pages return `sellerLocation` as the country only; turn on **Use the storefront's own language** to get the seller's province or city. Reviews come from Lazada's review list, 5 per page, and the run walks pages until it reaches **Max reviews per product** (or the product's last review). Detail pages are fetched best effort: a product whose page could not be read keeps its listing fields (price, original price, seller, location, rating, review count are all in the listing) and reports `enrichmentStatus: "detail-unavailable"`, and is not charged the detail surcharge. In recent test runs on Lazada Vietnam, detail pages could be read for about half of the products, so check `enrichmentStatus` and re-run the missing ones later rather than assuming every row is enriched.

#### Can I try it on the Apify Free plan?

Not reliably. The data this actor reads is only served to residential connections, and Apify Free plans do not include residential proxies (see Plan Requirement above), so a Free-plan run usually ends FAILED with an explanatory message. Two ways to test without a long commitment: use a plan that includes Residential proxy and run a small first job (`maxListings` of 5 to 10 with the optional enrichments off), or enter your own residential proxy URLs, located in Vietnam for `lazada.vn`, in the **Proxy** field.

#### Is it suitable for regular updates and a growing product database?

Yes. Schedule the same search with **Incremental mode** on and each run returns only new, changed and (optionally) expired products; **Resume** continues an interrupted walk; keep each run bounded with `maxListings` and `maxPages`. Plan for the two cost drivers: detail pages and reviews are the slow, proxy-heavy part, so refresh them less often than the listing prices, and expect an occasional product whose detail page cannot be read on a given run.

#### Is it legal to scrape Lazada?

This actor collects only publicly available product and review data. You are responsible for how you use it: follow Lazada's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product photos, videos and seller content may be subject to third party rights even though prices and specifications are generally facts.

#### Can I monitor a search or URL and get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared products, unchanged ones are not billed unless you turn on **Emit unchanged**, and expired products are only reported once a run has scanned the full tracked search without a cap.

#### What is the difference between Resume from a previous run and Incremental mode?

Resume (`resumeFromRunId`) continues one specific interrupted or very large run, skipping products a prior run already collected. Incremental mode remembers state itself across separate scheduled runs of the same search or URL configuration, classifying every product as new, updated, unchanged, reappeared or expired. Use Resume to finish or extend one walk, and Incremental mode for recurring monitoring.

#### Why did my run fail instead of returning an empty dataset?

A run fails when **Resume from a previous run** points at a run or dataset id this account cannot read, when **Incremental mode** conflicts with existing saved state for the same search, or when the site could not be reached at all on any connection. A run instead succeeds with an empty or smaller than expected dataset when no search queries or URLs were provided, when the country site and filters genuinely matched nothing, or when every page came back blocked after one retry, since a blocked page is treated as the end of that search rather than a fatal error. If detail enrichment is on but product pages are not reachable on the connection, the run still succeeds and returns listing-level fields only.

#### 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 a summary of each result into Notion, Linear or Airtable.

### 🔗 Want more e-commerce data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🏨 <a href="https://apify.com/abotapi/traveloka-com-scraper"><b>Traveloka Hotel Scraper</b></a><br>Scrape Traveloka hotels across Singapore, Malaysia, Indonesia, Thailand, Vietnam and the...</td><td>🛒 <a href="https://apify.com/abotapi/grabfood-restaurants-scraper"><b>GrabFood Restaurant Scraper</b></a><br>Scrape GrabFood restaurants across Southeast Asia, including SG, MY, TH, VN, PH, ID, KH...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/sephora-product-scraper"><b>Sephora Scraper</b></a><br>From $1/1K. Scrape Sephora products across 21+ storefronts (US, AU, NZ, SG, MY, ID, TH...</td><td>🛒 <a href="https://apify.com/abotapi/tokopedia-scraper"><b>Tokopedia Scraper</b></a><br>Extract product data from Tokopedia, Indonesia’s largest marketplace. Search by keyword...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/jobstreet-companies-reviews-scraper"><b>JobStreet</b></a><br>From $1/1K. Pull JobStreet company profiles and employee reviews across Malaysia...</td><td>🛒 <a href="https://apify.com/abotapi/shein-product-scraper"><b>SHEIN Scraper</b></a><br>Scrape SHEIN product listings by keyword or from any category, sale or search link...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/lazada-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

Pick how to start. 'search' runs query + filters below. 'url' walks one or more SERP / category / product URLs. Reviews-only output is a separate toggle below (works in either mode).

## `country` (type: `string`):

Lazada has six country sites. In search mode this picks the one site searched. In URL mode each URL's own host decides its site, so one run can mix sites, and this field is only the fallback for URLs on an unknown host. The connection's exit country always follows the site being fetched.

## `queries` (type: `array`):

One or more search terms. The actor runs a separate paginated walk for each query. Examples: 'laptop', 'iphone case', 'kopi'. Only used when mode = search. Defaults to \['laptop'], so a run that omits the field still scrapes.

## `categoryId` (type: `string`):

Deprecated: retired 2026-09-04 (#1553 audit). Lazada does not expose a numeric category id as a search-endpoint facet - live probes proved this field has no effect on results (a filtered run's item set was indistinguishable from the unfiltered baseline, and none of the returned items carried this id in their own category metadata). Lazada's real category browsing is a distinct URL per category (e.g. https://www.lazada.sg/shop-laptops/) - use mode = url and paste that category page's URL instead. Kept hidden for backward compatibility with existing saved tasks; has no effect either way, leave empty.

## `minPrice` (type: `integer`):

Minimum price in the country's local currency (SGD/MYR/THB/VND/PHP/IDR). No bound when empty.

## `maxPrice` (type: `integer`):

Maximum price in local currency. No bound when empty.

## `specialsOnly` (type: `boolean`):

When ON, walks Lazada's real Flash Sale campaign feed (/tag/flash-sale/) instead of the plain catalog — a genuinely different, curated set of products with real strike-through prices, not a client-side filter. Leave queries empty to browse the whole Flash Sale grid, or set queries to search within it. Every record also carries isOnSpecial/discountAmount/promoLabel/specialsCategory regardless of this toggle (LazMall and Campaign badges are detected on any card).

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

Sort the search results. Lazada's own SERP only exposes 3 real sort options (verified live 2026-09-04, #1553 audit): Best match, Price low to high, Price high to low. The last three entries in this list (rating, newest, best selling) are RETIRED: Lazada silently ignores those tokens and returns default-order results, so the actor now runs them as Best match. They are kept in the list only so existing saved tasks and schedules that still pass them keep starting; pick one of the first three for a new run.

## `urls` (type: `array`):

One or more URLs. Accepted shapes: keyword search, filtered/sorted/paginated search, category (/shop-<slug>/) and paginated category, Flash Sale feed (/tag/flash-sale/), tag landing page, product detail (slug and pdp-i<id> forms), and a store homepage (skipped with a warning, it has no listing). Each URL's country site is detected from its own host. Search filters above (price, sort, specials) are baked into the URL you paste. The rating filter below still applies in URL mode, and so does the free shipping filter beside it. Left untouched, URL mode scrapes one example of each shape.

## `minRating` (type: `integer`):

Drop products whose average rating is below this value (1 to 5). Applied client-side after the SERP fetch.

## `freeShippingOnly` (type: `boolean`):

Keep only items eligible for the country's free-shipping promotion, matched on each card's own Free Shipping badge. Applied client-side after the SERP fetch, so it narrows a keyword search AND a pasted search or category URL (in search mode the actor also asks Lazada's own free-shipping facet for the first pass).

## `localLanguage` (type: `boolean`):

Off by default. Lazada Vietnam search pages come back in English with the seller location reduced to the country ("Vietnam"). Turn this on to read them in Vietnamese instead: the seller location becomes the province or city (for example "Hà Nội" or "Thành phố Hồ Chí Minh") and titles are in Vietnamese. On Lazada Thailand it switches titles and the city name to Thai script. Other storefronts are unchanged. Applies to a keyword search and to a pasted search or category URL (a URL that already has its own lang keeps it). Switching it starts a new Incremental baseline, since titles change.

## `reviewsOnly` (type: `boolean`):

When ON, the actor walks the SERP / URLs to discover product IDs but emits ONE record per review (type='review'), not one per product. Works in both search and URL modes. When mode = url AND every URL points at a /products/ page, the run is dramatically faster and ~10x cheaper.

## `fetchReviews` (type: `boolean`):

When ON and reviewsOnly is OFF, every product record additionally carries an embedded array of its newest reviews (capped by maxReviewsPerProduct). Billed on the detail-enrichment event in blocks of 10 reviews returned per product (the first block is covered by the unit for a product page read); see the Pricing tab for the rate.

## `maxReviewsPerProduct` (type: `integer`):

Hard cap on reviews fetched per product. Lazada serves 5 reviews per page; the actor walks pages forward until the cap or the product's last review, so the default of 20 takes 4 page requests per product. Raise it for review depth analysis (each extra page is another request through your proxy). In Incremental mode only the newest 5 reviews of each product are compared for changes.

## `fetchDetails` (type: `boolean`):

When ON, the actor fetches each product's detail page after the SERP. Adds full SKU variant table, shipping + warranty, seller cluster, image gallery, breadcrumbs. Adds ~3-5 seconds per product. When OFF (default), only SERP-card fields populate (id, title, price, rating, image, seller name) at much lower cost.

## `detailMode` (type: `string`):

Retired and ignored. Product pages are always read in full when "Enrich each product with detail page data" is on. The field is kept only so saved tasks and API calls that still send it keep working.

## `maxPages` (type: `integer`):

Optional bound on SERP pages walked per starting query/URL. Lazada's SERP serves ~40 cards per page. Leave empty to walk result pages until maxListings is reached; a hard cap of 100 pages per query/URL always applies.

## `maxListings` (type: `integer`):

Hard cap across all queries / URLs. Set to 0 for unlimited (still bounded by maxPages). When reviewsOnly = true, this caps DISCOVERED products, not reviews; total review records can be much higher.

## `proxy` (type: `object`):

Apify Residential is required for SE Asia residential exits. The exit country always follows the Lazada site being fetched (a lazada.com.my URL or the Malaysia site uses a Malaysian exit even if the prefill says SG). Free-tier (Datacenter) is rejected by Lazada and the actor will warn at startup.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full-catalogue walk without returning or charging for products already collected there (matched by product id). Use this after an interrupted run, or to keep extending one walk-all pull across separate runs. For recurring daily/weekly monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. Each product is compared on every output field (price, discount, stock, rating, review count, sold count, seller, images, SKU list, vouchers and so on); fetchedAt and the raw card passthrough are always excluded, and you can exclude more with the ignore-fields option below. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately per mode + search/URL/filter configuration; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode + search/URL/filter settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max products total capped it, when Resume was used, or in reviews-only mode. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Output field names that must NOT make a product count as UPDATED, e.g. reviewCount or itemSold. Dotted paths reach inside lists (e.g. skuList.stock). The fields stay in the output and are only left out of the comparison and of changedFields. fetchedAt and raw are always ignored. Needs Incremental changes for scheduled runs turned on.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "country": "sg",
  "queries": [
    "laptop"
  ],
  "specialsOnly": false,
  "sortBy": "popularity",
  "urls": [
    "https://www.lazada.sg/catalog/?q=earbuds",
    "https://www.lazada.sg/catalog/?q=iphone&price=100-500&sort=priceasc&page=2",
    "https://www.lazada.sg/shop-laptops/",
    "https://www.lazada.sg/shop-laptops/?page=2",
    "https://www.lazada.sg/tag/flash-sale/",
    "https://www.lazada.sg/tag/earbuds/?q=earbuds&catalog_redirect_tag=true",
    "https://www.lazada.sg/products/la-gourmet-55l-crystalline-air-fryer-i3646864772.html",
    "https://www.lazada.com.my/catalog/?q=kopi"
  ],
  "freeShippingOnly": false,
  "localLanguage": false,
  "reviewsOnly": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 20,
  "fetchDetails": false,
  "detailMode": "full",
  "maxPages": 0,
  "maxListings": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `results` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `productReviews` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `summary` (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 = {
    "mode": "search",
    "queries": [
        "laptop"
    ],
    "urls": [
        "https://www.lazada.sg/catalog/?q=earbuds",
        "https://www.lazada.sg/catalog/?q=iphone&price=100-500&sort=priceasc&page=2",
        "https://www.lazada.sg/shop-laptops/",
        "https://www.lazada.sg/shop-laptops/?page=2",
        "https://www.lazada.sg/tag/flash-sale/",
        "https://www.lazada.sg/tag/earbuds/?q=earbuds&catalog_redirect_tag=true",
        "https://www.lazada.sg/products/la-gourmet-55l-crystalline-air-fryer-i3646864772.html",
        "https://www.lazada.com.my/catalog/?q=kopi"
    ],
    "maxPages": 0,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "SG"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/lazada-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 = {
    "mode": "search",
    "queries": ["laptop"],
    "urls": [
        "https://www.lazada.sg/catalog/?q=earbuds",
        "https://www.lazada.sg/catalog/?q=iphone&price=100-500&sort=priceasc&page=2",
        "https://www.lazada.sg/shop-laptops/",
        "https://www.lazada.sg/shop-laptops/?page=2",
        "https://www.lazada.sg/tag/flash-sale/",
        "https://www.lazada.sg/tag/earbuds/?q=earbuds&catalog_redirect_tag=true",
        "https://www.lazada.sg/products/la-gourmet-55l-crystalline-air-fryer-i3646864772.html",
        "https://www.lazada.com.my/catalog/?q=kopi",
    ],
    "maxPages": 0,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "SG",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/lazada-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 '{
  "mode": "search",
  "queries": [
    "laptop"
  ],
  "urls": [
    "https://www.lazada.sg/catalog/?q=earbuds",
    "https://www.lazada.sg/catalog/?q=iphone&price=100-500&sort=priceasc&page=2",
    "https://www.lazada.sg/shop-laptops/",
    "https://www.lazada.sg/shop-laptops/?page=2",
    "https://www.lazada.sg/tag/flash-sale/",
    "https://www.lazada.sg/tag/earbuds/?q=earbuds&catalog_redirect_tag=true",
    "https://www.lazada.sg/products/la-gourmet-55l-crystalline-air-fryer-i3646864772.html",
    "https://www.lazada.com.my/catalog/?q=kopi"
  ],
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "SG"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/lazada-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/lazada-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/CbjijXiuE0DSPyQEH/builds/j1MfJz3I50pMXCb0l/openapi.json
