# Lazada Product & Review Scraper - 6 Country Sites by URL (`lanternlab318/lazada`) Actor

Give it Lazada product URLs and get the public product and its reviews back, on all six Lazada country sites. No login and no cookies.

- **URL**: https://apify.com/lanternlab318/lazada.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.64 / 1,000 products

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

## Lazada Product & Review Scraper — 6 Country Sites by URL

A Lantern Lab Actor. Give it Lazada product URLs and get the public product back — title, SKU price, star rating, review count, category, seller id — together with that product's reviews: the stars, the review text, the review date, the SKU the reviewer bought and any review photos. Works on all six Lazada country sites, mixed freely in one run. No Lazada login, no cookies, no account of your own.

**Priced per row**: USD 0.64 per 1,000 products and USD 1.19 per 1,000 reviews, no run-start fee. Empty results, status rows and the run summary are free — a run that Lazada refuses costs you nothing.

### What it does

- Reads products from **Malaysia, Singapore, Thailand, the Philippines, Vietnam and Indonesia**. The country site is taken from each URL, so one run can mix them.
- Returns the product and its reviews from the same request, so a product's rating and review count come back even with reviews turned off.
- Computes `rating` from the star distribution. Lazada's own average field is unreliable — measured on a product with 54 five-star reviews, Lazada reported an average of `0`, so this Actor reports `5` and keeps Lazada's figure beside it as `ratingReported`.
- Keeps the source identifiers (`itemId`, `reviewId`), the platform's own review date (`publishedAt`, plus `publishedAtRaw` exactly as Lazada sent it) and our collection time (`collectedAt`) as separate fields. Every review row carries the `itemId` of its product, so the parent relation survives any export.
- Deduplicates across the whole run: a review Lazada repeats across pages is written once and charged once.
- Says what happened per product. Four outcomes are four different records and are never confused: a product Lazada answered with no reviews is an **empty result**; an item id Lazada answered for and does not carry is **`found: false`**; a product we never got an answer for is **`found: null`** with a reason, never counted as one Lazada does not have; a review list cut short by a limit, a refusal or an error is **`partial: true`** with the reason.
- Retries a refused request once from a residential exit in the product's own country, and from a **different** exit than the one that refused it. Lazada's Malaysian and Philippine sites answer the cheap datacenter proxy; the Singapore, Thai and Vietnamese sites refuse it, so those start on residential.
- Caps itself: `maxUpstreamRequests` is a hard ceiling on requests to Lazada for the whole run, including every retry, derived from your input when you leave it empty, so a run is never unbounded.

### What it does not do

- **No keyword search.** Lazada's catalogue is rendered in the browser from Alibaba's signed gateway and answers both datacenter and residential IPs with a bot check or a sliding captcha. This Actor takes product URLs, not search terms.
- **No `sold` figure.** Lazada serves units-sold only through that browser-only gateway. `sold` is therefore always `null` with `soldStatus: "not_published_server_side"` — it is never estimated.
- No pre-discount price: Lazada does not serve one on this endpoint, so `originalPrice` is `null`.
- No seller name: only `sellerId` is served here. No seller pages, no shop catalogues.
- No product description, specifications, variant list or images beyond the main one.
- No login-only, app-only or checkout data, and no buyer identity: only Lazada's own masked handle is returned, and a reviewer Lazada names in full is withheld.
- No scheduling or monitoring built in; use Apify schedules.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `productUrls` | required | One Lazada product URL per line, from any country site. A bare numeric item id also works and is read on `country`. |
| `country` | `MY` | Which site a bare item id belongs to: `MY`, `SG`, `TH`, `PH`, `VN`, `ID`. Full URLs use their own country instead. |
| `includeReviews` | `true` | Collect one row per review. Off gives a product-only run at one request per product. |
| `maxReviewsPerProduct` | `100` | Upper bound on review rows per product. Lazada serves 20 per request, so this rounds up to whole pages. |
| `residentialFallback` | `true` | Retry a refused request from a residential exit in the product's country. Off keeps a run strictly datacenter-only. |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to Lazada for the whole run. Leave empty and the Actor works one out from your input. |
| `proxyConfiguration` | Apify Proxy | The proxy the requests go through. The datacenter default is what this Actor is measured on for MY and PH. |

```json
{
  "productUrls": [
    "https://www.lazada.com.my/products/hanlu-mens-hoodies-i3718725294.html",
    "https://www.lazada.co.th/products/some-product-i2490024288.html"
  ],
  "includeReviews": true,
  "maxReviewsPerProduct": 100
}
```

### Output

One row per product, `recordType: "product"`:

| Field | Meaning |
| --- | --- |
| `itemId` | Lazada's own product identifier. Only valid on its own `country` site. |
| `country` | The Lazada country site this product was read from. |
| `title` | The product title as Lazada publishes it. |
| `price` | The price of **one SKU** as a number, read out of `priceRaw` — the variant Lazada pins the product to, not the product page's headline price. See `priceScope`, `priceSkuId` and the limits below. |
| `priceRaw` | The price exactly as Lazada sent it, for example `RM119.68`. The authoritative value. |
| `priceScope` | Always `"sku"`: it names what kind of price the row carries, so a caller does not have to assume it is the page price. |
| `priceSkuId` | Which SKU `price` belongs to. The row's `url` opens the product page on exactly that SKU, so the figure can be checked in one click. |
| `currency` | ISO currency of the country site, for example `MYR`. |
| `originalPrice` | Always `null` — Lazada does not serve a pre-discount price here. |
| `rating` | Average stars, computed from `ratingBreakdown`. `null` when nothing is rated. |
| `ratingReported` | The average Lazada itself reports. Kept for comparison; known to be wrong. |
| `ratingBreakdown` | How many reviews gave 5, 4, 3, 2 and 1 star. |
| `reviewCount` | How many reviews Lazada says the product has. |
| `sold` | Always `null`; see `soldStatus`. Never estimated. |
| `sellerId` | The seller's Lazada id. `sellerName` is `null` — this endpoint does not serve it. |
| `categoryName` | The product's category, plus `rootCategoryName` for the top-level one. |
| `live` | `false` when Lazada still knows the id but the listing is no longer live. |
| `url` | The product page. |
| `collectedAt` | When we read it. Lazada publishes no listing date, so `publishedAt` is `null`. |

One row per review, `recordType: "review"`:

| Field | Meaning |
| --- | --- |
| `reviewId` | Lazada's own review identifier. The dedupe key. |
| `itemId` | The product this review is about — the parent relation. |
| `rating` | Stars this review gave, 1–5. |
| `text` | The review text. `null` when the reviewer left only a rating. |
| `publishedAt` | The platform's review date as `YYYY-MM-DD`. Lazada publishes the **day only**, which `publishedAtPrecision` states — no time of day is invented. `null` when Lazada sent a relative date instead (`1 week ago`, about 6% of reviews measured): `publishedAtRaw` keeps it rather than turning it into a guessed day. |
| `publishedAtRaw` | The same date exactly as Lazada sent it. Lazada localises it per site, so this is `08 May 2025` on MY/SG/PH, `19 มี.ค. 2022` on TH and `31 thg 3 2025` on VN — all three are parsed into `publishedAt`. |
| `boughtAt` | When the reviewer bought it, when shown. |
| `buyerName` | Lazada's own masked handle, for example `S***y`. When Lazada shows an unmasked personal name instead, this is `null` and `buyerNameWithheld` is `true` — this Actor does not republish a reviewer's personal name. |
| `buyerNameWithheld` | `true` when Lazada showed an unmasked personal name and this Actor withheld it. |
| `skuInfo` | The variant the reviewer bought, for example `Color Family:Grey, Size:Int:3XL`. |
| `images` | Review photo URLs, or `null`. |
| `likeCount` | How many people marked the review helpful. |
| `sellerReplyCount` | How many seller replies the review has. |
| `page` / `rank` | Which review page it came from, and its position in this run's collection for the product. |

One row per requested product, `recordType: "product_status"` — never charged:

| Field | Meaning |
| --- | --- |
| `found` | `true` when Lazada carried a product for that item id, `false` when Lazada answered and carried none (a wrong id or a removed listing), and **`null` when we never got an answer at all** — refused, failed or never reached. "We could not read it" is never reported as "Lazada does not have it", and the run summary counts the three apart as `productsFound`, `productsNotFound` and `productsNotCollected`. |
| `live` | Whether the listing is still live. |
| `reviewsReturned` | Review rows this product wrote, and `reviewsAvailable` for what Lazada says it has. |
| `duplicatesSkipped` | Reviews Lazada repeated across pages, written once. |
| `partial` / `partialReason` | `true` means the review list is **incomplete** — never that there are no reviews. Reasons: `review_limit_reached`, `upstream_request_cap`, `charge_limit_reached`, `blocked`, `upstream_error`, `not_collected`. |
| `statusText` | A sentence saying what happened, so the dataset explains itself without the log. |

The last row is `recordType: "RUN_SUMMARY"`, also stored under the `RUN_SUMMARY` key: totals, how many products Lazada did not carry (`productsNotFound`) kept apart from how many we could not read (`productsNotCollected`), how many review lists are incomplete and why, how often Lazada's bot check refused us and how often the residential retry rescued it, and why collection stopped.

### Limits worth knowing

- **Item ids are per country.** The same number on another site is a different product or none at all.
- **A removed listing still answers.** Lazada echoes the id back with every other field null; that is `found: false`, not an error.
- **20 reviews per request.** `maxReviewsPerProduct` rounds up to whole pages.
- **Lazada's own average is unreliable**; use `rating`, not `ratingReported`.
- **SG, TH and VN need residential traffic**, which costs more than the datacenter default. Turn `residentialFallback` off to avoid it and those products come back `blocked`.
- **`price` is one SKU's price (`priceScope: "sku"`), not the product page's headline price.** Lazada pins every product it returns to a single variant — the one named in `skuInfo` and identified by `priceSkuId` — and `price` is that variant's price. When the pinned variant is not the one the product page opens on, the two differ, sometimes by a lot: item `4537447207` returns `RM8.12` for SKU `25627601319` (`Color:Black, Size:EU:40`) where the page's default variant shows `RM83.22`, and item `243168039` returns `RM3.15` where the page shows `RM3.08` — `RM3.15` being one of the variant prices printed on that same page. The row's `url` ends in `-s<priceSkuId>.html`, so it opens the page on the variant the price belongs to. **Use `price` as that SKU's price, not as "the product's price".**
- **Review dates are localised by Lazada**, not by the language you ask for. English, Thai, Vietnamese and Indonesian month forms are all read into `publishedAt`; a form that cannot be read leaves it `null` with `publishedAtRaw` kept.
- **`reviewCount` can exceed the reviews Lazada actually serves.** One measured product reported 17 reviews and served 10. The status row carries both `reviewsReturned` and `reviewsAvailable` so the gap is visible rather than hidden.
- Only publicly visible page data is collected. No login, no cookies, no personal contact details.

# Actor input Schema

## `productUrls` (type: `array`):

One Lazada product URL per line, from any of the six country sites - for example https://www.lazada.com.my/products/...-i3718725294.html. A bare numeric item id also works and is then read on the country chosen below. The country site is taken from each URL, so URLs from different countries can be mixed in one run. A product listed twice is collected once.

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

Which Lazada site a bare item id belongs to. Entries given as full URLs ignore this and use their own country. An item id is only valid on its own country site: the same number on another site is a different product or no product at all.

## `includeReviews` (type: `boolean`):

With this on, every product also gets one row per review: star rating, review text, review date, the SKU the reviewer bought and any review photos. Turn it off for a product-only run, which costs one request per product - the product's star rating and review count are returned either way.

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

Upper bound on the review rows one product may produce. Lazada serves 20 reviews per request, so this rounds up to whole pages. A product whose list is cut short by this setting is marked partial with reason "review_limit_reached", so a deliberate limit is never mistaken for a product running out of reviews.

## `residentialFallback` (type: `boolean`):

Lazada's Malaysian and Philippine sites answer the cheap datacenter proxy, while the Singapore, Thai and Vietnamese sites refuse it and answer only a residential exit in their own country. With this on, a refused request is retried once that way, and the sites known to refuse datacenter IPs start there. Turn it off for a strictly datacenter-only, cheapest run that reports those sites as blocked rather than paying for residential traffic.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Lazada, counting every residential retry. At the ceiling collection stops - the products not reached are written with partialReason "not_collected" and the run then ends normally. Leave empty and the Actor works a ceiling out from your products and limits, so a run is never unbounded.

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

The proxy the requests to Lazada go through. The default (Apify Proxy, datacenter) is the cheapest and is what this Actor is measured on for Malaysia and the Philippines. The residential retry above is configured separately and always matches the product's country.

## Actor input object example

```json
{
  "productUrls": [
    "https://www.lazada.com.my/products/hanlu-mens-hoodies-thick-sweatshirt-premium-monogram-print-sweatshirts-for-men-and-women-i3718725294.html"
  ],
  "country": "MY",
  "includeReviews": true,
  "maxReviewsPerProduct": 100,
  "residentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (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 = {
    "productUrls": [
        "https://www.lazada.com.my/products/hanlu-mens-hoodies-thick-sweatshirt-premium-monogram-print-sweatshirts-for-men-and-women-i3718725294.html"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/lazada").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 = { "productUrls": ["https://www.lazada.com.my/products/hanlu-mens-hoodies-thick-sweatshirt-premium-monogram-print-sweatshirts-for-men-and-women-i3718725294.html"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/lazada").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 '{
  "productUrls": [
    "https://www.lazada.com.my/products/hanlu-mens-hoodies-thick-sweatshirt-premium-monogram-print-sweatshirts-for-men-and-women-i3718725294.html"
  ]
}' |
apify call lanternlab318/lazada --silent --output-dataset

```

## MCP server setup

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

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/OtJ6tDoe4pb5MXo3l/builds/d5yFNQ5atZBFDlsAW/openapi.json
