# Panduro Craft & Hobby Store Scraper (`crawlerbros/panduro-scraper`) Actor

Scrape Panduro.com - a leading Nordic/European craft & hobby retailer (paint, yarn, beads, jewellery-making, kids' crafts). Search by keyword, browse any category, or fetch exact products by URL. Returns title, brand, price, stock, images and more.

- **URL**: https://apify.com/crawlerbros/panduro-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Agents, E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Panduro Craft & Hobby Store Scraper

Scrape live product data from [Panduro](https://www.panduro.com) — a leading
Nordic/European craft & hobby retailer covering paint, yarn, beads, jewellery-
making, kids' crafts and more. No login, no cookies, and no paid proxy
required.

### What this actor does

- **Search the catalog** — free-text keyword search, across four live storefronts: Sweden, Denmark, Finland and Norway
- **Browse a category** — give it a Panduro category listing page URL and it pages through the full listing for you
- **Lookup exact products** — fetch specific products by their page URL
- **Filters:** price, brand, category, stock, rating (search mode), on-sale (search mode)
- **Empty fields are omitted** — each product record is built from Panduro's own structured product data, and only fields the store actually publishes are included, no guessed or scraped-from-layout values

### Output per product

- `productId`, `sku`, `title`, `brand`, `description`
- `images[]`, `primaryImageUrl`
- `price` — current selling price, always populated for every mode
- `regularPrice` — pre-discount list price, only present when the product is discounted (mode=search only)
- `onSale`, `discountPercent` — only present when the product is discounted (mode=search only)
- `currency`, `inStock`
- `category` — most specific (last) level of the breadcrumb
- `categoryPath[]` — full breadcrumb trail from Panduro's own site navigation, e.g. `["Måla & rita", "Färg", "Akrylfärger"]`
- `averageRating`, `reviewCount` — only populated for mode=search; products with no reviews yet omit both fields
- `market` — which storefront locale the product was fetched from
- `sourceUrl`, `recordType: "product"`, `scrapedAt`

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search` | `search` / `byCategory` / `byProductUrls` |
| `market` | select | `sv-se` | Storefront: Sweden (`sv-se`), Denmark (`da-dk`), Finland (`fi-fi`), Norway (`nb-no`) — mode=search only, see FAQ |
| `searchQuery` | string | `paint` | Keyword (mode=search) |
| `categoryUrls` | array | – | Full category listing page URLs (mode=byCategory) |
| `productUrls` | array | – | Exact product page URLs (mode=byProductUrls) |
| `sortBy` | select | `popular` | `popular` / `newest` / `priceAsc` / `priceDesc` / `bestReviews` / `mostReviews` (mode=byCategory only) |
| `minPrice` / `maxPrice` | int | – | Price range (local currency of the selected market) |
| `brand` | string | – | Brand name contains this text |
| `category` | string | – | Any level of the category breadcrumb contains this text (e.g. `"Garn"`, `"Färg"`) |
| `inStockOnly` | boolean | `false` | Only emit products currently in stock |
| `minRating` | number | – | Minimum average rating 0-5 (mode=search only) |
| `onSaleOnly` | boolean | `false` | Only emit products currently discounted below their regular list price (mode=search only) |
| `maxItems` | int | `20` | Hard cap on emitted records (1-500) |

#### Example: search a market with price and rating filters

```json
{
  "mode": "search",
  "market": "sv-se",
  "searchQuery": "akrylfärg",
  "minPrice": 20,
  "minRating": 4,
  "maxItems": 30
}
```

#### Example: browse a category sorted by price

```json
{
  "mode": "byCategory",
  "categoryUrls": ["https://www.panduro.com/sv-se/sortiment/sy-sticka/garn"],
  "sortBy": "priceAsc",
  "inStockOnly": true,
  "maxItems": 40
}
```

#### Example: lookup exact products by URL

```json
{
  "mode": "byProductUrls",
  "productUrls": [
    "https://www.panduro.com/sv-se/produkt/exempel-produkt-123456"
  ]
}
```

#### Example: on-sale products in the Danish market

```json
{
  "mode": "search",
  "market": "da-dk",
  "searchQuery": "garn",
  "onSaleOnly": true,
  "maxItems": 25
}
```

### Use cases

- **Craft & hobby retailers** — track competitor pricing and stock across categories
- **Multi-market price comparison** — compare prices for the same brand across Sweden, Denmark, Finland, and Norway
- **Deal alerting** — watch for newly discounted products in a specific market
- **Catalog aggregation** — build a searchable export of Panduro's catalog for a given locale
- **Assortment research** — track category breadth and product availability over time

### Limitations

- **Only 4 of Panduro's storefronts are exposed: Sweden (sv-se), Denmark
  (da-dk), Finland (fi-fi) and Norway (nb-no).** Germany (de-de) and the
  Netherlands (nl-nl) are deliberately left out: their product search API
  returns zero results for every query tested (plain-language terms,
  brand names, empty term) and their category listing paths 404. This
  isn't a rendering/timing problem on our end — their storefront shells
  exist, but no product catalog is indexed behind them (Panduro's actual
  Dutch retail operation runs on the unrelated `pipoos.com` domain, outside
  this actor's scope). `mode=byProductUrls` still works for a product URL
  from any Panduro locale, including de-de/nl-nl, if you have one.
- `mode=byCategory` requires a full category listing URL copied from
  Panduro's own site navigation (each market uses a different URL path
  segment for categories — e.g. `/sv-se/sortiment/...` vs
  `/da-dk/products/...` vs `/fi-fi/tuotteemme/...`), rather than a
  category name/ID, since there's no single reliable cross-market category
  identifier exposed by the site.

### FAQ

**Does this need login or cookies?**
No. Panduro's product and category pages are public and unauthenticated.

**Does this use a paid/residential proxy?**
No. It runs entirely on Apify's own infrastructure with no proxy dependency.

**Can I browse a category instead of searching?**
Yes — set `mode` to `byCategory` and supply one or more category listing
page URLs in `categoryUrls` (copy them straight from Panduro's own category
navigation). The actor pages through the full listing and fetches full
detail for every product found.

**Can I control the order products come back in for `byCategory`?**
Yes — `sortBy` reorders the category listing itself (via Panduro's own
`sort` query parameter, the same one behind the category page's "Sortera"
widget): `popular` (Panduro's own default), `newest`, `priceAsc`,
`priceDesc`, `bestReviews`, `mostReviews`. Combine with `maxItems` to grab
just the top N by whichever order matters to you (e.g. `sortBy: priceAsc`

- `maxItems: 10` for the 10 cheapest products in a category). `sortBy` has
  no effect on `search` / `byProductUrls`, which don't have a Panduro-side
  listing to reorder.

**Which markets are supported, and why not all of them?**
Search covers Sweden (sv-se), Denmark (da-dk), Finland (fi-fi) and Norway
(nb-no) — all four were verified live to return real, in-stock and
out-of-stock products for a range of search terms. Germany (de-de) and the
Netherlands (nl-nl) are not offered: their catalogs are genuinely empty in
Panduro's own product search backend and their category pages don't
resolve. See "Limitations" above for the full explanation.

**Is this affiliated with Panduro?**
No, this is a third-party actor that reads Panduro's own public product and
category pages; it is not built or endorsed by Panduro.

**Why do `regularPrice`/`onSale`/`averageRating` only show up in `search` mode?**
Panduro's search index carries the pre-discount list price and rating data
alongside each result. `byCategory` and `byProductUrls` fetch the product
page directly instead, which only carries the current selling price — so
those modes never emit those fields, rather than guessing or showing a `0`.

**How current is the data?**
Every run fetches Panduro's live pages/search index at request time — prices
and stock reflect what's on the site right now, not a cached snapshot.

# Actor input Schema

## `mode` (type: `string`):

What to fetch.

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

Which Panduro country storefront to use for mode=search (mode=byCategory / mode=byProductUrls infer the market from the URLs you provide). Germany (de-de) and the Netherlands (nl-nl) are not offered: their catalogs returned zero results for every query tested, and their category paths 404 -- see the README's Limitations section.

## `searchQuery` (type: `string`):

Free-text keyword search.

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

Full Panduro category listing page URLs, e.g. https://www.panduro.com/sv-se/sortiment/sy-sticka/garn (Sweden) or https://www.panduro.com/da-dk/products/sy-strik/garn (Denmark). Copy the URL straight from Panduro's own category navigation for the market you want -- each market uses a different URL path segment.

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

Exact Panduro product page URLs.

## `minPrice` (type: `integer`):

Drop products priced below this value (in the market's local currency).

## `maxPrice` (type: `integer`):

Drop products priced above this value (in the market's local currency).

## `brand` (type: `string`):

Brand name contains this text.

## `category` (type: `string`):

Any level of the product's category breadcrumb (as shown on Panduro's own site navigation, e.g. "Garn" or "Bomullsgarn") contains this text.

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

Order to walk the category listing in. Comes from Panduro's own category page sort widget (confirmed live: each option returns a distinctly, correctly ordered product set). No effect on `search` / `byProductUrls`, which have no listing to reorder.

## `inStockOnly` (type: `boolean`):

Only emit products currently in stock.

## `minRating` (type: `number`):

Only emit products with an average customer rating (0-5) at or above this value. Rating data comes from Panduro's own search index and is only available for `mode=search` -- products with no reviews yet have no rating, so this filter (like `byCategory` / `byProductUrls`, which don't carry a rating field at all) drops them rather than treating a missing rating as 0.

## `onSaleOnly` (type: `boolean`):

Only emit products Panduro's own search index flags as currently discounted below their regular list price (`regularPrice` > current `price`). Discount data comes from Panduro's search index and is only available for `mode=search` -- `byCategory` / `byProductUrls` fetch the product page directly, which only ever carries the current selling price, not the original list price, so this filter has no effect there.

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

Hard cap on emitted records.

## Actor input object example

```json
{
  "mode": "search",
  "market": "sv-se",
  "searchQuery": "paint",
  "categoryUrls": [
    "https://www.panduro.com/sv-se/sortiment/sy-sticka/garn"
  ],
  "productUrls": [],
  "sortBy": "popular",
  "inStockOnly": false,
  "onSaleOnly": false,
  "maxItems": 20
}
```

# Actor output Schema

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

Dataset containing all scraped Panduro products.

# 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 = {
    "mode": "search",
    "market": "sv-se",
    "searchQuery": "paint",
    "categoryUrls": [
        "https://www.panduro.com/sv-se/sortiment/sy-sticka/garn"
    ],
    "productUrls": [],
    "sortBy": "popular",
    "inStockOnly": false,
    "onSaleOnly": false,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/panduro-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 = {
    "mode": "search",
    "market": "sv-se",
    "searchQuery": "paint",
    "categoryUrls": ["https://www.panduro.com/sv-se/sortiment/sy-sticka/garn"],
    "productUrls": [],
    "sortBy": "popular",
    "inStockOnly": False,
    "onSaleOnly": False,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/panduro-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "market": "sv-se",
  "searchQuery": "paint",
  "categoryUrls": [
    "https://www.panduro.com/sv-se/sortiment/sy-sticka/garn"
  ],
  "productUrls": [],
  "sortBy": "popular",
  "inStockOnly": false,
  "onSaleOnly": false,
  "maxItems": 20
}' |
apify call crawlerbros/panduro-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=crawlerbros/panduro-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/MM1CBaIYpzyqQmXMU/builds/GaK1QrJ4X6yYxdRH0/openapi.json
