# Pinkoi Scraper - Product, Price & New Listing Watch (`superslowsloth/pinkoi-products`) Actor

Pinkoi scraper: search keywords or shop ids to one row per listing with price and original price in 12 currencies (USD, JPY, TWD...), discount, rating, reviews, shop, photo, workshop and digital flags. For design-marketplace price and new-listing watching.

- **URL**: https://apify.com/superslowsloth/pinkoi-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.69 / 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/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

## Pinkoi Scraper - Product, Price & New Listing Watch

This Pinkoi scraper searches [Pinkoi](https://www.pinkoi.com), Asia's
cross-border design marketplace, by keyword or by shop and returns one flat row
per listing: the title, link and photo, the shop, the current price and the
price before the shop's discount in the **currency you choose** (twelve,
including USD, JPY, TWD, EUR and GBP), the discount in percent, rating, review
count, badges, whether the row is a sponsored slot, and whether it is an
ordinary product, a workshop or a digital download. Run it on a schedule with
the sort set to **Newest** and diff `product_id` against the last run to get a
**new-listing watch** for a keyword or a shop; run it with a fixed shop and
keyword and diff `price` to get a **price watch**.

No account, no API key, no login, no browser. One request returns up to 60 rows.

### Input

| Field | Notes |
|---|---|
| `searchTerms` | One or more search terms, one per line (`tote bag`, `ceramic mug`, `帆布袋`). Each is searched across all of Pinkoi. A listing found by two terms is returned once, under the first. |
| `shopIds` | One or more shops, one per line, whose whole catalogue you want. The ID is the end of the shop's address: `omhandmade` for `https://www.pinkoi.com/store/omhandmade` (the full address is accepted). With search terms as well, **each term is searched inside each shop**. |
| `maxItems` | Listings to return per term or shop (default 100, up to 10,000). The prefilled 30 is one page. |
| `sort` | `popular` (Pinkoi's default), `newest`, `price-asc`, `price-desc`: the choices of Pinkoi's own sort menu. |
| `currency` | The currency of every price: `USD` (default), `TWD`, `JPY`, `EUR`, `GBP`, `HKD`, `CNY`, `THB`, `SGD`, `MYR`, `AUD` or `CAD`. |
| `language` | The language of titles and shop names and of the product links: `en` (default), `zh-TW`, `zh-CN`, `zh-HK`, `ja` or `th`. |
| `includeSponsored` | On by default. Turn it off to return only organic listings; the sponsored rows skipped are not charged and do not count toward `maxItems`. |
| `proxyConfiguration` | Apify datacenter proxy by default. |

Give search terms, shop IDs or both. Example input:

```json
{
  "searchTerms": ["tote bag"],
  "shopIds": ["omhandmade"],
  "sort": "newest",
  "currency": "JPY",
  "language": "ja",
  "maxItems": 50
}
```

### Output

Prices are numbers in the currency you chose; `currency` repeats its code on
every row.

| Field | Notes |
|---|---|
| `query`, `shop_filter`, `position` | The term that found the listing (null when a whole shop was read), the shop the search was limited to (null for a search across Pinkoi), and the listing's place in Pinkoi's list for that term, shop and sort. 1 is first. It counts every row of the pages read, sponsored rows included, so it is the slot a visitor sees. |
| `product_id`, `url`, `image_url` | Pinkoi's product id (`7gYh5EN3`), the product page on the host of your language, and the first photo (500 px wide JPEG). |
| `name` | The listing title in your language. Line breaks inside a title are collapsed to a space. |
| `shop_id`, `shop_name` | The shop's id (what `shopIds` takes) and its name. |
| `kind` | `product`, `workshop` (an experience or class, Pinkoi's "Workshop" badge) or `digital` (a downloadable file). |
| `workshop_location` | Where a workshop takes place (`Taipei City`). Null for other kinds. |
| `currency`, `price` | The ISO code you asked for, and the current price in it. |
| `original_price`, `on_sale` | The price before the shop's discount, and whether it is above `price`. |
| `discount_percent` | `(1 - price / original_price) * 100`, one decimal place. Worked out from the two prices, not sent. **Null when not on sale**, never 0. |
| `rating`, `review_count` | The average stars (1 to 5, one decimal) and the number of reviews. Both null for a listing with no reviews. |
| `has_video` | True when the listing has a video. |
| `badges` | Pinkoi's feature badges as codes: `customize`, `exclusive`, `eco`, `readytobuy` ("N people are about to buy", which changes by the hour), `social_good`. An empty list when there are none. |
| `is_sponsored` | True when the row was a sponsored slot. |
| `category_id`, `subcategory_id` | Pinkoi's category numbers. Pinkoi sends no category name. |

Example row (real values, 2026-11-02):

```json
{
  "query": "tote bag",
  "shop_filter": null,
  "position": 67,
  "product_id": "Tr8iP5Kw",
  "name": "【MOOMIN】Canvas Multi-Pocket Tote Bag",
  "url": "https://en.pinkoi.com/product/Tr8iP5Kw",
  "image_url": "https://cdn01.pinkoi.com/product/Tr8iP5Kw/0/2/500x0.jpg",
  "shop_id": "yoshi850",
  "shop_name": "Iam select shop",
  "kind": "product",
  "currency": "USD",
  "price": 19.2,
  "original_price": 28.81,
  "on_sale": true,
  "discount_percent": 33.4,
  "rating": null,
  "review_count": null,
  "has_video": false,
  "badges": [],
  "is_sponsored": false,
  "category_id": 1,
  "subcategory_id": 104,
  "workshop_location": null
}
```

#### Sponsored rows, repeats and order

Pinkoi mixes sponsored listings into its results: a page is 30 organic
products plus up to 30 sponsored ones (60 rows), and later pages carry fewer as
the ad pool runs out. The same product can appear twice on a page, once
sponsored and once organically (all 12 products of one small shop came back
twice). Each product is returned **once**: as the organic row when a page has
both, otherwise the first sighting. The repeats are not charged.

Pinkoi's order is not fixed. Two requests for the same search list slightly
different rows in a slightly different order, and the match count drifts by a few
(`tote bag` read 21,792 to 21,800 over a handful of requests). Positions are the order
of the run that produced them. The price sorts are Pinkoi's own and not exact:
in a sample of 160 organic rows, 16 neighbouring pairs were out of order
ascending and 44 descending.

#### Currency

Pinkoi picks the currency from the country of the visitor, and a guest cannot
change it on the site, so a scraper behind a proxy would get the proxy's
currency: yen from Japan, US dollars from the United States. This actor always
asks for the currency you chose and checks that every price came back in it;
a page priced in another currency is refused and retried, never written under
the wrong code. Prices are Pinkoi's own conversion, so the same listing in
two currencies is two numbers, and a listing's price can move with the exchange
rate without the shop changing anything. For a price watch keep the currency fixed.

#### Honest nulls

A field Pinkoi did not send is `null`, never `0` or `""`. `badges` is an empty
list when there are none.

- **`price` can really be 0.** A digital listing priced at 0.00 with an
  `original_price` above it is free (`discount_percent` 100). Pinkoi sends it.
- **No listing date.** Pinkoi's search list carries no creation date, so
  `newest` is Pinkoi's own "Most recent" order, which the data cannot be used to
  check (the sponsored slots at the top of a page stay the same under every sort).
  A new-listing watch is a diff of `product_id` between runs.
- **No stock, no description, no variants.** Pinkoi's search list carries
  neither; a row is a listing at its listed price.
- **No shipping information.** Pinkoi's free-shipping badge depends on the
  visitor's country (10 of 60 rows from a Japanese address, none from an Apify
  datacenter address), so it is not reported.

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.000693** per product written (`product-scraped`), i.e. $0.693 per 1,000
  listings. A row that is not written is not charged: sponsored rows skipped by
  `includeSponsored`, and repeats of a product already returned, are free.

### How it fetches

Pinkoi's search pages answer a script with a "Please wait" challenge (HTTP 403).
The page fills its list from a JSON service,
`/apiv3/search/search_product`, which needs no key, cookie or login and which
this actor reads directly. Measured 2026-11-02: it answers JSON to a plain
connection in Japan, to an Apify datacenter address (10 of 10 in the build
probe), to the residential group (10 of 10, and 10 of 10 pinned to Japan) and to
the platform's own address. A refusal (403, 429,
5xx), a 200 that is not Pinkoi's answer, or prices in the wrong currency is
treated as the address being blocked, and the actor rotates to a fresh one and
retries.

Things worth knowing, all measured:

- **The language is the host, not a setting.** `en.pinkoi.com`, `www.pinkoi.com`
  (Traditional Chinese, Taiwan), `cn.`, `hk.`, `jp.` and `th.` search the same
  catalogue; the language input picks the host and so the titles and the links.
- **A shop is a filter.** `shopIds` limits the search to one shop's listings
  (`omhandmade`: 4,103 listings; with `linen`, 764 of them). A shop that does
  not exist is an empty answer, logged, not an error and not charged. Shop IDs are
  lower case and exact.
- **Depth.** Pinkoi answers up to page 1,000 (page 1,001 is refused with
  `search_exceed_max_window_limit`). The actor stops there and says so in the log.
  A page past the last real page is an empty list. At most 10,000 listings per
  term or shop; a broad term can match more than 100,000 listings, so narrow it
  with a shop or a more specific term.
- **Photos.** The photo is built from the listing id and the image revision the
  list sends; the 40 tried (22 different revisions) all answered with a JPEG.

If Pinkoi changes its search service, the run fails with a message instead of
returning empty rows.

# Actor input Schema

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

What you would type into the Pinkoi search box, one term per line (for example tote bag, ceramic mug or notebook). Each term is searched across all of Pinkoi, in any language Pinkoi's search understands. A listing that two terms both find is returned once, under the first. When you also give shop IDs, each term is searched inside each of those shops instead.

## `shopIds` (type: `array`):

Pinkoi shops whose whole catalogue you want, one per line. A shop ID is the last part of its address: for https://www.pinkoi.com/store/omhandmade it is omhandmade (the full address is accepted too). Letters, digits, - and \_ only; an ID is lower case. With search terms as well, each term is searched inside each shop. A shop ID that does not exist returns no rows and is not charged.

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

Return at most this many listings for each search term (or shop). A page holds 30 listings plus the sponsored ones Pinkoi mixes in, so up to 60. Up to 10,000 are accepted; Pinkoi lists nothing past its 1,000th page.

## `sort` (type: `string`):

The order Pinkoi lists the results in, the same choices as its own sort menu. 'Most popular' is its default. 'Newest' is Pinkoi's 'Most recent' sort, for catching new listings. The price sorts are Pinkoi's own and are not exact: neighbouring listings can be slightly out of order.

## `currency` (type: `string`):

The currency Pinkoi shows the prices in. Pinkoi converts every price into it, so the same listing has a different number in each currency. Without a choice Pinkoi would use the currency of the proxy's country, so this is always set; US dollars by default.

## `language` (type: `string`):

The language of the listing titles and shop names, and of the product links. The same listing reads differently in each language (an English title in 'English', the Chinese one in 'Traditional Chinese (Taiwan)'). It does not change which listings are found.

## `includeSponsored` (type: `boolean`):

Pinkoi mixes sponsored listings into its results (the is_sponsored field). Turn this off to return only organic listings. Sponsored listings that are skipped are not charged and do not count toward Max listings.

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

Use Apify's automatic datacenter proxy (the default). Pinkoi's search API answered Apify datacenter addresses with HTTP 200 when this actor was built; the residential group was not needed. A refused request is retried from a fresh address.

## Actor input object example

```json
{
  "searchTerms": [
    "tote bag"
  ],
  "shopIds": [],
  "maxItems": 30,
  "sort": "popular",
  "currency": "USD",
  "language": "en",
  "includeSponsored": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "searchTerms": [
        "tote bag"
    ],
    "shopIds": [],
    "maxItems": 30,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/pinkoi-products").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 = {
    "searchTerms": ["tote bag"],
    "shopIds": [],
    "maxItems": 30,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/pinkoi-products").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 '{
  "searchTerms": [
    "tote bag"
  ],
  "shopIds": [],
  "maxItems": 30,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/pinkoi-products --silent --output-dataset

```

## MCP server setup

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

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/uPhJkqBM6m2dvj27s/builds/bmRWq5EkWe1MKHoq9/openapi.json
