# Yupoo Scraper (`skyfishdev/yupoo-scraper`) Actor

Scrape Yupoo stores, categories, or single albums: images, titles, descriptions, and album URLs.

- **URL**: https://apify.com/skyfishdev/yupoo-scraper.md
- **Developed by:** [Ibrahim](https://apify.com/skyfishdev) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 1,000 product scrapeds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 🖼️ Yupoo Scraper — Albums, Images, Titles, Descriptions

Point it at a Yupoo **store**, **category**, or **single album** and get clean structured data back. No login, no browser automation, no guesswork.

This Actor does one job properly: it reads Yupoo pages and hands you exactly what is on them — image URLs, titles, descriptions, album URLs. It does not score, rank, rewrite, or "enrich" anything. What you see in the output is what the seller published.

***

### 💡 Who is this for?

- **Catalog and inventory work** — pulling a supplier's full album list into a spreadsheet or database.
- **Developers** building on top of Yupoo data who want a clean JSON feed instead of writing an HTML parser.
- **Anyone doing bulk image collection** from albums that hold 30–100 photos each.

***

### 🎯 What it does

| You give it | You get back |
| --- | --- |
| A **store** URL — `https://STORE.x.yupoo.com/albums` | Every album in the store |
| A **category** URL — `https://STORE.x.yupoo.com/categories/5280382` | Every album in that category |
| A **single album** URL — `https://STORE.x.yupoo.com/albums/246388734?uid=1` | That one album |
| A **bare store name** — `chaosmade20` | Same as the store URL |

Mix as many as you like in one run — each URL is detected automatically, no mode switch to set.

***

### ⚡ Quick start

**Just the album URLs from a store** (fastest and cheapest — reads index pages only, never opens an album):

```json
{ "urls": ["chaosmade20"], "albumUrlsOnly": true }
```

**Everything from one category:**

```json
{ "urls": ["https://chaosmade20.x.yupoo.com/categories/5280382"] }
```

**Only the images from a single album:**

```json
{
  "urls": ["https://chaosmade20.x.yupoo.com/albums/246388734?uid=1"],
  "fields": ["images"]
}
```

> 💡 **Start small.** Run one album first and check the output is shaped the way you need before pointing it at a store with thousands of albums. Yupoo stores vary a lot — some put their whole catalog on one page, some paginate, some leave descriptions empty.

***

### 📤 Output

One record per album, pushed straight to the **Dataset** — no file keys to hunt for, no post-processing step. Export as JSON, CSV, or Excel from the Storage tab, or pull it from the API.

```json
{
  "store": "chaosmade20",
  "albumUrl": "https://chaosmade20.x.yupoo.com/albums/246388734?uid=1",
  "title": "Washed Black Tee",
  "description": "https://example.com/products/tee",
  "images": [
    "https://photo.yupoo.com/chaosmade20/95d571333a/big.jpg",
    "https://photo.yupoo.com/chaosmade20/a87080adfd/big.jpg"
  ]
}
```

`store` and `albumUrl` are always present. Everything else is whatever you selected in `fields` — **unselected fields are left out entirely, not returned as empty strings or nulls.**

***

### ⚠️ Read this before you import images anywhere

**Yupoo image URLs are hotlink-protected.** This is a property of Yupoo, not of this Actor, and it will affect you no matter which scraper you use. Measured directly:

| How the image is requested | What Yupoo returns |
| --- | --- |
| No `Referer` header | `HTTP 567` — a 7 KB HTML error page, not an image |
| `Referer` from your own site | `HTTP 567` — blocked |
| `Referer: https://STORE.x.yupoo.com/` | `HTTP 200` — the real JPEG (337 KB) |

**What this means in practice:**

- Pasting an image URL straight into a browser tab shows a broken image. The URL is still correct.
- Embedding these URLs directly in a storefront or web page will not work.
- Downloading them **does** work — send a `Referer` header of `https://STORE.x.yupoo.com/` with the request:

```bash
curl -H "Referer: https://chaosmade20.x.yupoo.com/" \
  "https://photo.yupoo.com/chaosmade20/95d571333a/big.jpg" -o image.jpg
```

Once downloaded, host the files wherever you like and the restriction no longer applies.

#### ✅ The Actor already fixes this for you

**Rehosting is on by default.** Every image is downloaded from Yupoo using the referer that defeats the block, re-uploaded, and returned as a **working URL in the `images` field** — links that display anywhere, in any storefront, with no referer tricks. You do not have to configure anything.

To use your own image-host account instead of the built-in one, set `imageHostApiKey`. To skip rehosting entirely and get raw Yupoo URLs — much faster and lighter — set `rehostImages: false`.

```json
{
  "urls": ["https://chaosmade20.x.yupoo.com/albums/246388734?uid=1"],
  "imageHostApiKey": "YOUR_KEY"
}
```

| `imageHost` | Get a key at | Notes |
| --- | --- | --- |
| `freeimage` | [freeimage.host](https://freeimage.host/page/api) | Default. Chevereto API. |
| `imgbb` | [api.imgbb.com](https://api.imgbb.com/) | Supports link expiry. |
| `custom` | — | Any Chevereto-compatible v1 endpoint via `uploadEndpoint`. |

**How it behaves:**

- **Keys are validated before the run starts.** A wrong or blocked key fails in seconds instead of dying three thousand images into a long job.
- **If no key is available at all, the run still completes** — you get every album with its original Yupoo URLs, plus a warning in the log. The scrape is never lost because of an image-host problem.
- **A failed upload keeps its original Yupoo URL** rather than dropping out of the results. Your record is never left short — but a run can return a mix of rehosted and original URLs, so make sure your import handles both.
- **The run log reports the split** — how many images were rehosted and how many kept their originals.
- **`imageLinkExpirySeconds: 0`** (the default) keeps images forever. Expiry is an ImgBB feature and is ignored by hosts that do not support it.
- **Resolution is preserved, but the file is re-encoded.** Measured on freeimage.host: a 1080×1080 source came back 1080×1080, with the JPEG about 18% smaller. You keep full dimensions; you do not get a byte-identical copy. If you need the exact original file, download it yourself with the referer method above.

> ⚠️ **Free image hosts have their own limits and terms.** Bulk-uploading tens of thousands of images may get you rate limited or blocked. Uploads go to your own account, so quotas and content rules are between you and that host. If one blocks you, switch `imageHost` rather than rewriting anything.

***

### 💰 What a run costs

There is **no charge from this Actor for rehosting** — you bring your own free image-host key, and that host bills you nothing on its free tier. The cost that matters is Apify **platform usage**, which is driven by requests and data transfer. Measured from real runs:

| Mode | Requests | Data moved |
| --- | --- | --- |
| `albumUrlsOnly: true` | 1 per **~77–120 albums** (index pages only) | Negligible |
| `rehostImages: false` | 1 per album, plus index pages | ~50–150 KB per album |
| **Default (rehosting on)** | 1 per album **+ 2 per image** | **~780 KB per image** |

The default row is the one to plan around. Each image is downloaded (~340 KB) and re-uploaded as base64, which inflates it by roughly a third (~440 KB). Albums hold 30–100 images, so a single album moves 25–75 MB.

**A worked example:** 100 albums averaging 50 images each is 5,000 images ≈ **3.9 GB** of transfer and 10,000 upload round trips. That is a long, heavy run.

**How to keep it cheap:**

- Use `albumUrlsOnly` when you only need the catalog structure — it is roughly **100× cheaper** than a full scrape.
- Set **`rehostImages: false`** for exploratory runs. Turn it back on for the albums you are actually importing.
- Use `maxImagesPerAlbum` to cap rehosting if you only need a few photos per product.
- Set `fields` to just what you need. It does not reduce requests (all fields come from the same page) but it does shrink your dataset.
- **Always test on one album first** and read the run's usage before scaling to a store.

***

### 📥 Input reference

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `urls` | array | — | **Required.** Album, category, or store URLs — mix freely. Bare store names work. |
| `fields` | array | all three | Any of `images`, `title`, `description`. |
| `albumUrlsOnly` | boolean | `false` | List album URLs without opening each album. Dramatically faster. |
| `maxAlbumsPerStore` | integer | `100` | Per store or category URL. `0` = no limit. |
| `maxPages` | integer | `5` | Album-index pages to walk. |
| `imageSize` | string | `big` | `small`, `medium`, or `big`. Costs no extra requests — see below. |
| `maxImagesPerAlbum` | integer | `0` | `0` = no limit. |
| `albumConcurrency` | integer | `4` | Album pages fetched at once. |
| `requestDelayMs` | integer | `250` | Minimum pause before each request. Raise if you see `429`s. |
| `maxRetries` | integer | `3` | Retries per failed request. |
| `rehostImages` | boolean | `true` | **On by default.** Re-uploads images so the URLs work anywhere. Set `false` for a faster, lighter run. |
| `imageHost` | string | `freeimage` | `freeimage`, `imgbb`, or `custom`. |
| `imageHostApiKey` | string | — | Optional — use your own image-host account. Stored as a secret. |
| `uploadEndpoint` | string | — | Only for `imageHost: "custom"`. |
| `imageLinkExpirySeconds` | integer | `0` | `0` = never expire. ImgBB only. |
| `imageConcurrency` | integer | `4` | Images downloaded and uploaded at once. |
| `proxyConfiguration` | object | Apify Proxy | Yupoo is served from China and can be slow or unreachable from some regions. |

***

### 🔍 Notes from the field

Things learned the hard way, documented so you do not have to rediscover them:

- **Album URLs need their `uid` query parameter.** Strip it and Yupoo replies `200 OK` with a body reading *"this album is not exist"* — not a `404`. A naive scraper writes that as a valid empty record. This Actor keeps the parameter and detects the body anyway, so a dead album is reported rather than silently returned.
- **Two gallery layouts are live simultaneously.** Older stores render `album3__main` cards, newer ones `album__main`. Both are handled.
- **Titles live in three different places** depending on layout. When a page forces a fall back to `<title>`, that value carries a ` | album | STORE | …` suffix, which is stripped before output.
- **Not every store paginates.** Some render the entire catalog on page one. Pagination stops as soon as a page yields nothing new instead of walking empty pages.
- **Albums are large.** Across the stores tested, albums held roughly 30–100 images each. That is why `maxImagesPerAlbum` defaults to no limit — a silent cap would quietly discard most of your data.
- **Image size is a URL rewrite.** Yupoo serves `/small.jpg`, `/medium.jpg`, and `/big.jpg` from the same path, so choosing `big` costs nothing extra.
- **Descriptions are freeform.** Sellers write whatever they want there — sometimes a product link, sometimes measurements, sometimes nothing. You get the text as-is.

***

### 🔌 API integration

Replace `YOUR_TOKEN` with your token from **Apify Console → Settings → API & Integrations**.

#### Small jobs — one call, results returned inline

For a single album or a small category, run synchronously and get the data straight back:

```bash
curl -X POST "https://api.apify.com/v2/acts/Vw1EDApmqzgczTA1a/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://chaosmade20.x.yupoo.com/albums/246388734?uid=1"],"fields":["images"]}'
```

The response is the array of results. No polling, no second request.

#### Large jobs — start, poll, fetch

Big stores take a while, so start the run asynchronously:

```bash
curl -X POST "https://api.apify.com/v2/acts/Vw1EDApmqzgczTA1a/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["chaosmade20"],"maxAlbumsPerStore":0}'
```

Save `data.id` (the run ID) and `data.defaultDatasetId` from the response, poll until the status is `SUCCEEDED`, then download:

```bash
curl "https://api.apify.com/v2/datasets/DATASET_ID/items?token=YOUR_TOKEN&format=json" -o output.json
```

#### Full script (Node.js)

Save as `scrape.mjs` and run with `node scrape.mjs`.

```js
const ACTOR = 'Vw1EDApmqzgczTA1a';
const TOKEN = 'YOUR_APIFY_TOKEN';
const BASE = 'https://api.apify.com/v2';

const INPUT = {
  urls: ['chaosmade20'],       // store, category, or album URLs
  fields: ['images', 'title', 'description'],
  maxAlbumsPerStore: 0,        // 0 = no limit
};

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getJson = async (url) => (await fetch(url)).json();

// 1. Start the run
const start = await (
  await fetch(`${BASE}/acts/${ACTOR}/runs?token=${TOKEN}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(INPUT),
  })
).json();

const { id: runId, defaultDatasetId: datasetId } = start.data;
console.log('Run:', runId);

// 2. Poll until it finishes
let status = start.data.status;
while (['RUNNING', 'READY'].includes(status)) {
  await wait(30_000);
  ({ data: { status } } = await getJson(`${BASE}/actor-runs/${runId}?token=${TOKEN}`));
  console.log('Status:', status);
}
if (status !== 'SUCCEEDED') throw new Error(`Run ended as ${status}`);

// 3. Download the results
const items = await getJson(`${BASE}/datasets/${datasetId}/items?token=${TOKEN}&format=json`);
const { writeFileSync } = await import('node:fs');
writeFileSync('output.json', JSON.stringify(items, null, 2));
console.log(`Saved ${items.length} albums to output.json`);
```

***

### 🧰 What this Actor does not do

Stated plainly so you can judge fit before spending anything:

- **No AI or LLM processing.** Prices, SKUs, and sizes are not extracted from description text. Yupoo has no price field — sellers type prices into freeform text, so any extraction is guesswork. Across the stores tested, price text was absent entirely from 1 in 4 of them. You get the raw description and can parse it yourself if your catalog is consistent.
- **No video extraction.** Images only.
- **No password-protected albums.**

***

### 🖥️ Running it locally

```bash
npm install
```

```bash
npm test
```

Put your input in `storage/key_value_stores/default/INPUT.json`, then:

```bash
node src/main.js
```

Results land in `storage/datasets/default/`.

***

### ⚖️ Responsible use

This Actor reads publicly accessible pages. It does not bypass authentication, rate limits, or access controls, and it ships with a request delay and conservative concurrency — please leave them in place.

You are responsible for what you do with the output. Images and listings on Yupoo are usually someone else's intellectual property. Republishing or selling based on scraped content may carry legal obligations depending on the content and your jurisdiction. Check before you build on it.

# Actor input Schema

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

Album, category, or store URLs - each one is detected automatically. A store or category URL is expanded into its albums; an album URL is scraped directly. Bare store names work too.

## `fields` (type: `array`):

What to pull from each album page. Unselected fields are left out of the results entirely.

## `albumUrlsOnly` (type: `boolean`):

For store and category URLs, stop after listing album URLs instead of opening each album. Far faster and cheaper, since it only reads the index pages.

## `maxAlbumsPerStore` (type: `integer`):

Stop after collecting this many albums from each store or category URL. Set to 0 for no limit.

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

How many pages of the album index to walk. Some stores put their whole catalog on one page.

## `imageSize` (type: `string`):

Which Yupoo image variant to return. This is a URL rewrite, so larger sizes cost no extra requests.

## `maxImagesPerAlbum` (type: `integer`):

Cap the images returned per album. Defaults to 0 (no limit) — albums commonly hold 30 to 100 images.

## `albumConcurrency` (type: `integer`):

How many album pages to fetch at once. Keep this low to stay polite and avoid rate limiting.

## `requestDelayMs` (type: `integer`):

Minimum pause before each request made by a worker. Raise this if you see 429 responses.

## `maxRetries` (type: `integer`):

How many times to retry a failed request before giving up on it.

## `rehostImages` (type: `boolean`):

On by default. Downloads each image from Yupoo and re-uploads it so the returned URLs work anywhere. Yupoo blocks hotlinking, so without this the image URLs will not display on your own site. Turn it off for a faster, lighter run when you only need the raw Yupoo URLs.

## `imageHost` (type: `string`):

Where to re-upload images. freeimage.host and ImgBB both offer free API keys.

## `imageHostApiKey` (type: `string`):

Your own API key for the selected host. Required when "Rehost images" is on. Validated at the start of the run.

## `uploadEndpoint` (type: `string`):

Only used when Image host is "Custom". Full URL of a Chevereto-compatible v1 upload endpoint.

## `imageLinkExpirySeconds` (type: `integer`):

0 keeps images forever. Otherwise 60 to 15552000 (180 days). Only ImgBB supports expiry; ignored on other hosts.

## `imageConcurrency` (type: `integer`):

How many images to download and upload at once. Raising this risks ImgBB rate limiting.

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

Yupoo is served from China and can be slow or blocked from some regions. A datacenter proxy usually resolves it.

## Actor input object example

```json
{
  "urls": [
    "https://unionkingdom.x.yupoo.com/albums",
    "https://zs888.x.yupoo.com/albums/123456789?uid=1"
  ],
  "fields": [
    "images",
    "title",
    "description"
  ],
  "albumUrlsOnly": false,
  "maxAlbumsPerStore": 100,
  "maxPages": 5,
  "imageSize": "big",
  "maxImagesPerAlbum": 0,
  "albumConcurrency": 4,
  "requestDelayMs": 250,
  "maxRetries": 3,
  "rehostImages": true,
  "imageHost": "freeimage",
  "imageLinkExpirySeconds": 0,
  "imageConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Dataset containing one item per scraped album.

## `runStats` (type: `string`):

Counts of discovered, scraped, missing, and failed albums and image uploads.

# 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 = {
    "urls": [
        "https://unionkingdom.x.yupoo.com/albums?tab=gallery"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("skyfishdev/yupoo-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 = { "urls": ["https://unionkingdom.x.yupoo.com/albums?tab=gallery"] }

# Run the Actor and wait for it to finish
run = client.actor("skyfishdev/yupoo-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 '{
  "urls": [
    "https://unionkingdom.x.yupoo.com/albums?tab=gallery"
  ]
}' |
apify call skyfishdev/yupoo-scraper --silent --output-dataset

```

## MCP server setup

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