# Lazada Scraper (`s-r/lazada-scraper`) Actor

Scrape Lazada products across all six marketplaces: Singapore, Malaysia, Indonesia, the Philippines, Thailand and Vietnam. Search by keyword, browse a category or read a single product, and get the price with its currency, the seller, the brand, the rating and the stock state on every row.

- **URL**: https://apify.com/s-r/lazada-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 catalogue rows

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

Read Lazada product data across all six Lazada marketplaces: **Singapore,
Malaysia, Indonesia, the Philippines, Thailand and Vietnam.** Search by
keyword, browse a category, or pull a single product in full detail. Every row
comes back with the price, the **currency that price is in**, the marketplace
country, the seller, the brand, the rating and the stock state.

Southeast Asia is six separate retail markets wearing one brand. The same
search returns a different catalogue, different sellers and different prices in
each one, and the prices are denominated in six different currencies. This
Actor treats that as the main event rather than a footnote.

### What you get

One row per unique product. Duplicates across pages are removed before
anything is counted or billed.

| Field | What it is |
| --- | --- |
| `item_id` | Lazada's product id, unique within a marketplace |
| `sku_id` | The specific variant the listed price belongs to |
| `name` | Product title as the seller wrote it |
| `url` | Canonical product page |
| `image` | Main product image |
| `price` | Current selling price, as a number |
| `price_currency` | ISO 4217 code for that price: SGD, MYR, IDR, PHP, THB or VND |
| `price_display` | The price exactly as the marketplace renders it |
| `original_price` | Pre-discount price when the product is on offer |
| `discount` | The discount the marketplace advertises, as text |
| `rating` | Average customer rating out of 5 |
| `review_count` | How many reviews sit behind that rating |
| `sold_count_display` | Units sold, when the marketplace shows it |
| `in_stock` | Whether the product is flagged available |
| `is_sponsored` | True when the product was placed as an ad, not earned by relevance |
| `seller_id`, `seller_name` | Who is selling it |
| `brand_id`, `brand_name` | Brand on the marketplace |
| `ships_from` | Where it ships from, which is often not the marketplace country |
| `market`, `market_country` | Which of the six marketplaces the row is from |
| `matches_query` | Whether a search row genuinely relates to your term |
| `page`, `position_on_page` | Where it ranked, so you can rebuild the results |
| `source`, `source_input` | Which term or URL produced the row |
| `scraped_at` | UTC timestamp of the read |

Product detail rows add the full seller description, the category path, the
variant list, the availability statement and a currency code read from the
product page itself.

### Why `price_currency` is on every row

Lazada does not tell you what currency a price is in. The currency field on
every product it returns is empty, and the only hint on the response is a bare
symbol such as `$` or `RM`. A dollar sign is not a currency. If you pool a
Singapore catalogue with a Philippine one and both prices arrive as bare
numbers, you now have a price history that is quietly wrong, and nothing in
the data will ever tell you.

So this Actor derives the denomination from the marketplace itself, which is
the one thing in the request that actually fixes it, and stamps
`price_currency` on every single row. For product detail rows it goes further:
the product page states a real currency code, and that code is compared
against the marketplace. If the two ever disagree, the row says so in
`currency_agrees_with_host` and the run summary lists the mismatch, rather
than picking one and hoping.

### Why a search sometimes returns nothing on purpose

This is the part most Lazada tooling gets wrong, and it is worth understanding
before you build on the output.

When Lazada cannot match your search term, it does not return an empty page.
It returns a full page of forty completely unrelated products, reports success,
and prints a line claiming that thousands of items were found. There is no
error code, no empty flag and no clue in the response. A scraper that trusts
the response hands you forty rows of confident nonsense, under a run that
reports success.

This Actor measures how much of each result page actually relates to what you
asked for, by checking your search words against the product names, brands and
sellers that came back. A genuine search scores close to 100 percent. An
unmatched search scores zero. When a page falls below the threshold, the
products are discarded and the search term is listed under `noMatchQueries` in
the run summary, so you learn that the marketplace had nothing rather than
inheriting its consolation prizes.

You can turn this off with **Keep recommended products when a search has no
real match**. The products are then kept but flagged `matches_query: false`
and `is_recommendation_fallback: true`, so they still cannot be mistaken for
real results.

The same scepticism is applied to the marketplace's own result counts. Lazada
reports the same total for every query, matched or not, because the number is
a ceiling rather than a count. It is passed through as
`claimedMatchCountsUnverified` and clearly labelled, never as a fact.

### Read the run summary

The run summary is not a log, it is part of the output. It exists so that a
run returning fewer rows than you expected tells you which of several very
different things happened.

- `outcome` — `ok`, `partial`, `blocked`, `no_matches`, `empty` or `no_input`
- `priceCurrency` — the currency of the marketplace you selected
- `currenciesInOutput` — the currencies actually present in the dataset,
  which is usually one and is the field to check before you sum a column
- `marketOverrides` — any URL you pasted that belonged to a different
  marketplace than the one you selected. Those rows are priced in their own
  marketplace's currency, never the one from the dropdown
- `itemCount` and `billableCount` — what you got and what you paid for
- `noMatchQueries` — terms the marketplace could not match
- `paginationStops` — why each term stopped, per term: reached your limit,
  ran out of pages, started repeating itself, or was refused
- `deadProducts` — product URLs that still serve a page but no longer have a
  product on it
- `currencyMismatches` — any row where the page and the marketplace disagreed
- `pagesFetched`, `requestAttempts`, `attemptsRefused` — how much work the run
  did and how much of it was turned away

An empty dataset with `outcome: no_matches` means the marketplace genuinely
has nothing for your term. An empty dataset with `outcome: blocked` means the
run never got in and you should try again. Those are opposite conclusions and
no Actor should let you confuse them.

### Pagination

One page is 40 products. Set **Pages per search term** to walk deeper, up to
25 pages per term in a single run. Lazada itself serves at most 102 pages for
any query, which is roughly four thousand products, and a run stops early and
says so when the marketplace reports no further pages, when a page starts
repeating products already returned, or when your product limit is reached.

Searching one term across several pages costs one request per page, so deep
pagination is cheap. Product detail is one request per product and is the
expensive half, which is why it is billed separately.

### Inputs

- **Marketplace** — which of the six, and therefore which currency
- **Search terms** — keywords, one per line
- **Category URLs** — copy them from your browser; filters in the URL are kept.
  A URL from another marketplace is read on its own marketplace and priced in
  its own currency, and the run summary tells you it did that
- **Product URLs or item IDs** — full detail for specific products. A bare ID
  is read on the marketplace you selected; a full URL is read on its own
- **Maximum products** — stop after this many unique products
- **Pages per search term** — how deep to go
- **Sort results by** — popularity, price ascending or descending, or newest
- **Minimum query relevance** — the no-match threshold described above
- **Keep recommended products when a search has no real match**
- **Attempts per page** — how hard to retry a page the marketplace declines

### Typical uses

**Price monitoring across Southeast Asia.** Run the same search term on all
six marketplaces and compare, safely, because every row carries its own
currency and country. Sponsored placements are flagged so they do not pollute
a ranking study.

**Competitor and seller research.** `seller_id`, `seller_name` and
`ships_from` show who is actually selling a category in each market and where
the stock comes from. A surprising share of Southeast Asian listings ship
cross-border, and `ships_from` is how you spot it.

**Catalogue and assortment mapping.** Walk a category several pages deep to
see what a market actually stocks, and which brands hold shelf space.

**Product research.** Feed in item IDs you already care about and get the full
description, the variants and the category path.

**Market entry sizing.** Rating and review counts across a category are a
usable proxy for demand, and they differ sharply between the six markets.

### Notes and limits

- Six marketplaces. Lazada does not operate anywhere else.
- Prices, sellers and stock are what the marketplace shows an anonymous
  visitor. Member-only and app-only prices are not visible and are not
  invented.
- `ships_from` is the marketplace's own claim, not a verified origin.
- A product id that no longer exists still serves a page. Those are reported
  under `deadProducts` rather than silently dropped.
- Runs are paced deliberately. This is a courtesy to the marketplace and it is
  also what keeps the success rate high.

### Free plan

Free Apify accounts receive up to 10 rows per run. The limit is applied before
the first request rather than after the work is done, so a free run does not
sit waiting for pages it will never receive. Paying accounts are not limited.
Billing follows delivery: you are charged for the rows you actually received,
never for rows the limit removed.

# Actor input Schema

## `market` (type: `string`):

Which Lazada marketplace to read. The marketplace decides the currency of every price in the output: Singapore is SGD, Malaysia MYR, Indonesia IDR, the Philippines PHP, Thailand THB, Vietnam VND. Products, prices and sellers differ per marketplace, so the same search returns a different catalogue in each.

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

Keywords to search, one per line. Each term is searched on the selected marketplace and returns up to 40 products per page. If the marketplace has no real match for a term it answers with unrelated recommended products instead of an empty page, and this Actor detects that and reports the term under noMatchQueries rather than handing you the recommendations as results.

## `categoryUrls` (type: `array`):

Lazada category or browse URLs to read, one per line. Copy them straight from your browser address bar, for example https://www.lazada.sg/shop-mobiles/. Filters already applied in the URL are preserved. A URL on a different marketplace than the one selected above is moved onto the selected marketplace host.

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

Individual products to read in full detail, one per line. Accepts a full product URL or just the numeric item ID. Detail rows add the brand, the full description, the category path, the variant list and a currency code read from the product page itself. A product that no longer exists is reported under deadProducts instead of being silently skipped.

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

Stop after this many unique products across all search terms, categories and product URLs. Duplicates across pages are removed before this is counted. Free Apify plans receive at most 10 rows per run and this Actor lowers the target to match before it makes its first request, so a free run does not wait for pages it will not receive.

## `pagesPerSearch` (type: `integer`):

How many result pages to walk for each search term and each category URL. One page is 40 products. The marketplace serves at most 102 pages for any query, and this Actor also stops early when a page repeats products it has already returned, when the marketplace says there are no more pages, or when the product target is reached. The reason it stopped is reported per term under paginationStops.

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

The ordering the marketplace applies to search results before this Actor reads them. Popularity is the marketplace default and is the best match ordering. Price ascending and descending are useful for finding the cheapest or most expensive offers for a term. Ignored for category URLs that already carry their own sort.

## `minQueryRelevance` (type: `integer`):

How much of a result page must actually relate to your search term before the page is accepted, as a percentage. Lazada answers an unmatched search with 40 recommended products and still reports thousands of matches, so this Actor measures how many returned product names, brands or sellers contain a word from your search term. A genuine search scores close to 100. A search with no real match scores 0. Lower this only if you search in a language where your terms do not appear in product titles.

## `keepUnmatchedResults` (type: `boolean`):

Off by default. When the relevance check above fails, the products the marketplace offered instead are discarded and the search term is reported under noMatchQueries. Turn this on to keep those products anyway; they are then flagged with matches\_query false and is\_recommendation\_fallback true on every row, so they can never be mistaken for real search results.

## `attemptsPerPage` (type: `integer`):

How many times to retry a page that the marketplace declines to serve. Lazada answers a declined request with a 200 response that contains no products at all, so retrying is how a page gets read rather than a nicety. Raise this if runs come back with a blocked outcome, lower it to fail faster. Every attempt spent is reported as requestAttempts in the run summary.

## Actor input object example

```json
{
  "market": "sg",
  "searchTerms": [
    "phone case"
  ],
  "categoryUrls": [],
  "productUrls": [],
  "maxItems": 40,
  "pagesPerSearch": 1,
  "sortBy": "popularity",
  "minQueryRelevance": 25,
  "keepUnmatchedResults": false,
  "attemptsPerPage": 6
}
```

# Actor output Schema

## `products` (type: `string`):

One row per unique product, each carrying its price, its currency and its marketplace country.

## `summary` (type: `string`):

outcome, itemCount, priceCurrency, currenciesInOutput, pagesFetched, attemptsRefused, noMatchQueries, paginationStops and deadProducts. Read this before concluding a marketplace has nothing for you.

## `errors` (type: `string`):

Per-page failures with a code and a message. Absent when the run had none.

# 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 = {
    "market": "sg",
    "searchTerms": [
        "phone case"
    ],
    "categoryUrls": [],
    "productUrls": [],
    "maxItems": 40,
    "pagesPerSearch": 1,
    "sortBy": "popularity",
    "minQueryRelevance": 25,
    "keepUnmatchedResults": false,
    "attemptsPerPage": 6
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = {
    "market": "sg",
    "searchTerms": ["phone case"],
    "categoryUrls": [],
    "productUrls": [],
    "maxItems": 40,
    "pagesPerSearch": 1,
    "sortBy": "popularity",
    "minQueryRelevance": 25,
    "keepUnmatchedResults": False,
    "attemptsPerPage": 6,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "market": "sg",
  "searchTerms": [
    "phone case"
  ],
  "categoryUrls": [],
  "productUrls": [],
  "maxItems": 40,
  "pagesPerSearch": 1,
  "sortBy": "popularity",
  "minQueryRelevance": 25,
  "keepUnmatchedResults": false,
  "attemptsPerPage": 6
}' |
apify call s-r/lazada-scraper --silent --output-dataset

```

## MCP server setup

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