# Shein Catalog Scraper - Product IDs, Titles & Images (`dami_studio/shein-catalog-scraper`) Actor

One row per entry in Shein's public XML catalogue: product id, full title, product URL, image URL and the date Shein last changed it. Thirteen storefronts, millions of entries each. No prices - Shein publishes none anywhere this can read. Keyless, no browser.

- **URL**: https://apify.com/dami\_studio/shein-catalog-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** E-commerce, SEO tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.20 / 1,000 catalogue entries

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

## Shein Catalog Scraper: product ids, titles and images from the public catalogue

**There are no prices in this output, and there is no setting that turns them on.** That is the
first thing to know, because price is what most people are after when they go looking for Shein
data, and this is not where they will find it.

What Shein does publish, openly and in bulk, is the catalogue itself: an XML index of every product
URL on a storefront, each entry carrying the product id, the **full product title**, a CDN image URL
and a last-modified date. Germany lists 440 catalogue files of 12,000 entries each, which is
5,271,135 products. Twelve more storefronts publish their own, about 60 million rows between them. This
Actor reads that, one row per entry, and stops there. It does not open product pages, because the
product pages are closed: tested across five different HTTP clients on four storefronts, every one
came back with the product data stripped out. No price, no stock, no rating, no sizes, no seller.

So: if you need prices, buy something else. If what you need is **the list**, every product id and
title on a storefront, everything whose title mentions "cargo", an id set you can diff against last
week's, this reads it cheaply and without a browser, a login or a key.

### What one row looks like

A real row from a real run:

```json
{
  "ok": true,
  "recordType": "catalogEntry",
  "region": "de",
  "contentType": "products",
  "productId": "68470079",
  "title": "2pcs/Set Baby Girls Newborn Cute Bodysuit & Headband Summer Outfit",
  "url": "https://de.shein.com/2pcs-Set-Baby-Girls-Newborn-Cute-Bodysuit-%26-Headband-Summer-Outfit-p-68470079.html",
  "imageUrl": "https://img.ltwebstatic.com/v4/j/spmp/2025/05/24/78/1748082702.jpg",
  "lastmod": "2026-09-20",
  "changefreq": "daily",
  "price": null,
  "sourceSitemap": "sitemap-products-1.xml",
  "scrapedAt": "2026-09-20T17:42:11.108Z"
}
```

`price` is on every row, always null. It is there so that anyone scanning the output for a price
finds the field and its answer rather than an absent key they have to guess about.

### Input

Everything is optional. Run it with no input at all and you get one free sample row showing the
shape, and nothing is charged for it.

```json
{
  "region": "de",
  "maxItems": 500,
  "updatedSince": "2026-09-19"
}
```

| Field | What it does |
|---|---|
| `region` | Which storefront to read. Thirteen publish a catalogue file: `de`, `uk`, `it`, `es`, `pt`, `nl`, `pl`, `fr`, `ro`, `at`, `se`, `ch`, `tr`. Each keeps its own ids, its own URLs and titles in its own language. |
| `maxItems` | How many rows you want, up to 100,000. The run stops the moment it has them and cancels the download mid-file, so 200 rows costs a fraction of 20,000. |
| `contentType` | `products` is the catalogue, and the only one with titles and pictures. `categories` and `stores` are the other two families of URL in the same index. Much smaller, and carrying only a URL and a date. |
| `titleContains` | Up to 20 words. A row is kept if its title or URL contains any one of them, case-insensitive. |
| `updatedSince` | A date, `YYYY-MM-DD`. Keeps only entries stamped on or after it. Read the paragraph below before you use it. It does not mean what you would reasonably expect. |
| `maxSitemapFiles` | How many catalogue files one run may open, up to 20. One file is 12,000 entries, so nine covers the 100,000-row limit. This is the bound on how far a narrow filter will hunt before it gives up. To go deeper, move `startFile` on and run again. |
| `startFile` | Where in the catalogue to begin. Files do not overlap: file 1 and file 221 were checked and share none of their 12,000 ids. So note where you finished and start the next run there. |

Both filters are applied here, to rows already read, because there is no search behind a static XML
file. There is only the file. The upside is that a filter cannot be quietly ignored the way a query
parameter can, and rows it drops are never delivered or charged. The downside is that a narrow filter
makes the run read a lot and deliver a little, which costs you time even though it costs you no rows.
That is what `maxSitemapFiles` bounds.

**`updatedSince` is not a change feed, and this is the one setting that will mislead you.** Shein
re-stamps the whole catalogue at once rather than each product when it changes. Checked on
2026-09-20: two catalogue files read end to end were 12,000 of 12,000 on a single date, all 440
files in the index carried that date, and so did samples taken from 24 files spread across the
whole catalogue. So the date tells you when Shein last regenerated the catalogue, not which products
moved. Set it to yesterday and you may get everything; set it to tomorrow and you will get nothing.

If you want real changes, pull the ids and diff them against your last pull. The setting is still
here because it does exactly what it says and costs nothing, and if a run drops every row to it you
get a free row naming the dates it actually saw rather than a silent zero. One thing to expect:
runs from different places see slightly different stamps, and a run may come back carrying today's
date and yesterday's side by side. Within any one catalogue file the date is the same on every
entry, which is the part that matters.

### Output

One row per catalogue entry, with these fields:

| Field | Notes |
|---|---|
| `region` | The storefront the row came from. |
| `contentType` | `products`, `categories` or `stores`. |
| `productId` | Shein's own id, pulled out of the URL. Null on category and store rows, which have no product id. |
| `title` | The full product title as Shein publishes it. On category and store rows, where Shein publishes no title at all, it is read off the URL instead. |
| `url` | The product page. |
| `imageUrl` | A CDN image. Empty on category and store rows, which carry none. |
| `lastmod` | The date on Shein's own stamp, `YYYY-MM-DD`. One date for the whole catalogue, refreshed daily. It is not a per-product change date; see the input section. |
| `changefreq` | Shein's own hint about how often it changes. |
| `price` | Always null. See the top of this page. |
| `sourceSitemap` | Which catalogue file the row came out of, so you can resume from it. |
| `scrapedAt` | When the run read it. |

Every real row carries `ok: true` and `recordType: "catalogEntry"`. The free sample row carries
`_sample: true` and every diagnostic carries `_diagnostic: true`, both with `charged: false`. So a
filter on `ok === true` gives you real data and nothing else, whatever else the run had to report.

### Price

**$0.20 per 1,000 rows**, plus a **$0.0015 start fee per run**. One event per row written to the
dataset is the only thing metered, flat on every plan, with no volume tiers and no minimum spend.

| Rows | Total cost |
|---|---|
| 1,000 | $0.2015 |
| 12,000 (one catalogue file) | $2.4015 |
| 100,000 | $20.0015 |

A storefront holds millions of entries, so the row count you ask for is the bill you get. There is
no per-file minimum hiding underneath it.

Everything else is free. The sample row is free. Diagnostic rows are free. A row dropped by one of
your filters is never delivered and never charged. A catalogue file that comes back refused is free,
and so is one that comes back empty. A run that matches nothing at all charges for no rows at all.

The metered event is a named one, so there is nothing quietly riding on the built-in dataset-item
charge. That is the usual reason a run ends up costing more than the figure on the page.

### How it works

Shein publishes the catalogue as an XML index at each storefront's own address. No key, no session,
no browser, and the files answer a plain request. The run reads the index first, works out which
files it needs, and then streams them.

Streaming rather than downloading matters here for two reasons. One file decodes to roughly eight
megabytes, and holding one, never mind two, inside a small container is how a run gets killed
halfway through. And because the reader parses entries as they arrive, `maxItems` stops the transfer
in the middle of a file instead of after it. Asking for 200 rows genuinely transfers about 200 rows'
worth of bytes.

The run checks what came back rather than trusting the status code. A refusal dressed as a page, a
file that parses to nothing, a file whose entries carry no usable URL: each produces a free
diagnostic row saying which file and why, and the run carries on to the next one. A `startFile`
past the end of the catalogue is refused before anything is downloaded, and the message tells you
how many files the storefront actually has today.

Requests go out directly by default, because that is what the files answer to, and the run retries automatically if a request is ever refused. You can point the
run at proxy servers of your own if you would rather, but you do not need to.

### Limitations: what this does not do

- **No prices.** Not on any row, not on any setting, not on any storefront. Shein does not put a
  price in the catalogue and the product pages are closed to this kind of request. This is the
  limitation that matters most and it is not going to change from this side.
- **No stock, sizes, colours, ratings, reviews or seller data either.** Same reason. The catalogue
  is a list of what exists, not a description of it.
- **Thirteen storefronts.** `us`, `www`, `mx`, `in`, `ar`, `ru`, `br`, `ca`, `au` and `jp` all
  answer 403 to the catalogue file, and there is no setting that gets round that. Thirty-two
  country codes were checked, each at two possible addresses; thirteen serve it. Mostly European,
  but Switzerland and Turkey are on the list and the United States is not, so whatever draws the
  line is not a border anyone can name from the outside.
- **`lastmod` cannot tell you what changed.** It is one date stamped across the entire catalogue
  and refreshed daily. Covered above. It is the single most misleading field in the output and it
  is Shein's, not ours.
- **Titles are in the storefront's language.** The German catalogue is mostly English in practice,
  but the French and Polish ones are not, and there is no translation step.
- **Category and store rows are thinner than product rows.** Those files carry a URL and a date
  and nothing else, so the title is derived from the URL slug and the image is empty.
- **One run is one storefront.** Reading four means four runs.
- **A narrow filter is slow, not cheap-and-fast.** It still has to read entries to reject them.
  It costs you nothing per rejected row, but it costs you wall-clock, and `maxSitemapFiles` will
  stop it before it reads the whole catalogue.
- **The catalogue is not a stock list.** An entry existing does not mean the product is buyable,
  and a product vanishing from the catalogue is the only signal you get that it went away.

### FAQ

**Can I get Shein prices from this?**
No. Not with a setting, not with a different region, not with your own proxy servers. The catalogue
files carry no price and the product pages do not serve one to this kind of request.

**How many products are there?**
Germany lists 440 files of 12,000 entries each, which is 5,271,135 products once you allow for the
short last file. The United Kingdom has slightly more, Turkey the fewest at 307 files. Files within
a storefront do not overlap, so the numbers add up rather than double-count.

**How do I pull the whole catalogue?**
Run it repeatedly, moving `startFile` forward. One run can take 100,000 rows, which is about nine
files, so note the `sourceSitemap` on your last row and start the next run there.

**How do I find out what changed since last week?**
Pull the ids and compare them yourself. `updatedSince` will not do it; see the input section. Ids
that appear are new listings and ids that vanish are gone, and that is the only change signal the
catalogue carries.

**Do I need an API key, a login or a proxy?**
No, no and no. The files are public and answer an ordinary request.

**What happens if Shein refuses a file mid-run?**
You get a free row naming the file and the status it returned, the run switches address and keeps
going, and you are charged for nothing that did not arrive.

**Why is there a `price` field if it is always null?**
Because an absent field makes people wonder whether they did something wrong. A field that is
present and empty, with a README paragraph explaining it, does not.

**Is this legal to use?**
It reads files Shein publishes for search engines to read, at a polite rate, with no login and no
attempt to get past anything. What you then do with the data is your call and your responsibility.

# Actor input Schema

## `region` (type: `string`):

Which Shein storefront to read. These thirteen publish a catalogue file; the other nineteen checked answer 403, 404, 410 or nothing at all, so there is nothing to add to this list. Each storefront keeps its own ids, its own URLs, and titles in its own language.

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

How many rows to return. The run stops the moment it has this many and cancels the download in the middle of the file, so asking for 200 genuinely transfers about 200 rows' worth of bytes rather than the whole 12,000-entry file. Keep it low while you are testing - you pay per row.

## `contentType` (type: `string`):

Products is the catalogue itself, and the only one with titles and images. Category pages and store pages are the other two families of URL in the same index: far smaller, and carrying only a URL and a date, so their title is derived from the URL and their image is empty.

## `titleContains` (type: `array`):

Case-insensitive. A row is kept if its title or its URL contains any one of these. Up to 20 terms. Leave it empty to take everything.

## `updatedSince` (type: `string`):

A date as YYYY-MM-DD. READ THIS BEFORE YOU SET IT: Shein re-stamps the whole catalogue at once, not each product when it changes. Measured on 2026-09-20: two files read end to end were 12,000 of 12,000 on a single date, all 440 files in the index carried that date, and samples from 24 files across the whole catalogue matched. So this is a gate on when Shein last regenerated the catalogue, not a list of what changed - set it to yesterday and you may get everything, set it to tomorrow and you will get nothing. To find genuine changes, pull the ids and diff them against your last pull. If a run drops every row to this filter it tells you which dates it actually saw.

## `maxSitemapFiles` (type: `integer`):

Shein splits the catalogue into files of 12,000 entries each. This caps how many one run opens, which is the real bound on how long a narrow filter can hunt before giving up, and on what a fruitless hunt costs. With no filter set one file is 12,000 rows, so nine files cover the 100,000-row limit. To look deeper than twenty files, move the start file forward and run again. The run stops at whichever it reaches first, this or the row limit, and the log says which.

## `startFile` (type: `integer`):

Where in the catalogue to begin. The files do not overlap - file 1 and file 221 were checked and share none of their 12,000 ids - so this is how you walk the whole catalogue across several runs without repeating yourself. Note the file your last row came from and start the next run there. A number past the end is refused before anything is downloaded, and the message tells you how many files that storefront actually has today.

## `proxyUrls` (type: `array`):

Leave this empty for a normal run. Fill it in only if you want the traffic to leave through proxy servers you already pay for, one URL per line, in the form http://user:pass@host:port.

## Actor input object example

```json
{
  "region": "de",
  "maxItems": 200,
  "contentType": "products",
  "maxSitemapFiles": 10,
  "startFile": 1
}
```

# Actor output Schema

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

Every row in the default dataset: region, contentType, productId, title, url, imageUrl, lastmod, changefreq, price, sourceSitemap, scrapedAt. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "region": "de",
    "maxItems": 200,
    "contentType": "products",
    "maxSitemapFiles": 10,
    "startFile": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/shein-catalog-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 = {
    "region": "de",
    "maxItems": 200,
    "contentType": "products",
    "maxSitemapFiles": 10,
    "startFile": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/shein-catalog-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 '{
  "region": "de",
  "maxItems": 200,
  "contentType": "products",
  "maxSitemapFiles": 10,
  "startFile": 1
}' |
apify call dami_studio/shein-catalog-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/shein-catalog-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/MWmOH9lrk81UV0Tlc/builds/d0x0u9nn1j3kbbeuh/openapi.json
