# Thomann Product & Price Scraper (Germany, EUR) (`superslowsloth/thomann-products`) Actor

Thomann price scraper: search Europe's largest music retailer by keyword and get one row per product: EUR price, struck-through and RRP price, discount, stock status, rating, review count, brand, image and URL. For guitar, synth and pro-audio price watching.

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

## Pricing

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

## Thomann Product & Price Scraper (Germany, EUR)

Search [Thomann](https://www.thomannmusic.com), Europe's largest music retailer,
for one or more keywords and get one flat row per product: the selling price in
**EUR** (German VAT included), the struck-through earlier price and the
manufacturer's recommended price where Thomann shows them, the discount, the
stock status, rating, review count, Thomann's own sales rank, brand, image and
product URL. Run it on a schedule and diff the dataset to get a **music-gear
price watch**: track what a Shure SM58, a Boss pedal or a Fender Stratocaster
costs today against last week, and when a sold-out item comes back.

No account, no API key, no login, no browser. One request returns 100 products.

### Input

| Field | Notes |
|---|---|
| `searchTerms` | One or more search terms, one per line. Required. `shure sm58`, `boss ds-1`, `fender player ii stratocaster`. A product found by two terms is returned once, under the first. |
| `maxItems` | Products to read per search term (default 100, up to 10,000). 100 per page. |
| `sort` | `popularity` (default), `price-asc`, `price-desc`, `best-rated`, `newest`, `name-asc`, `name-desc`. |
| `proxyConfiguration` | Apify proxy, automatic by default. |

#### The store is pinned to Germany

The actor reads `www.thomannmusic.com/en-de/`, Thomann International for Germany:
English text, **EUR**, German VAT included. It does not use `www.thomann.de/intl/`,
because that address sends every visitor to the store of their own country
(measured 2026-10-15: a Japan exit lands on `/en-jp/` in JPY, an Apify datacenter
exit in the United States in USD). A response that turns out to be another store
is treated as the exit address being wrong: nothing is billed for it, and the
actor retries from a fresh address. Product links point at the same Germany store
the prices come from.

#### About search terms

Thomann redirects a term that names a **brand or a category** to that brand's or
category's own page (`fender`, `stratocaster`). Those are real product lists and
are read as such, in the order you asked for. A **broad category word** with no
product list at all (`guitar`) is redirected to a navigation page; the actor then
searches it again as the exact phrase (`"guitar"`) and logs that it did. Put a
brand or model in the term (`harley benton sc-450`) for a tight, predictable list.
An empty term is refused: an empty Thomann search is the whole 125,000-product
catalogue.

#### About the sorts

The sort is **confirmed from Thomann's own answer** on every page: the page says
which sort it applied, and an unknown sort is silently ignored by Thomann, so a
term fails (at no charge) rather than return rows in an order that was not asked
for. Thomann's "popularity" is its default order.

### Output

Prices are numbers in euros; `currency` is always `EUR`.

| Field | Notes |
|---|---|
| `query`, `position` | The search term that first found the product, and its place in Thomann's result list for that term and sort (1 is the first result). |
| `product_id`, `url` | Thomann's article number (`105767`) and the product page on the Germany store. |
| `name`, `brand`, `model`, `subtitle` | The full title (`Shure SM58 LC`), the manufacturer, the model without the brand, and Thomann's one-line product type (`Dynamic Cardioid Vocal Microphone`). |
| `price` | The selling price now, VAT included. |
| `original_price`, `on_sale`, `discount_percent`, `discount_amount` | `original_price` is the struck-through earlier price on a reduced product. `on_sale` is true when it is above `price`. `discount_percent` (one decimal) and `discount_amount` are worked out from the two prices and are **null when the product is not reduced**, never 0. |
| `retail_price`, `retail_discount_percent` | The manufacturer's recommended price (UVP) where Thomann shows one, and how far `price` sits below it. Null otherwise. |
| `price_includes_vat`, `vat_rate` | As sent (`true`, `19.0`). Gift certificates carry no VAT. |
| `unit_price`, `unit_price_unit` | Price per metre, litre or similar on goods sold that way (`0.22` per `m`). Null on everything else. |
| `availability_text`, `availability_status`, `in_stock` | Thomann's stock line (`In stock`, `In stock within 2-3 weeks`, `Product is sold out`), its machine tag (`in-stock`, `on-date`, `short-term`, `mid-term`, `temporarily-not-available`, `sold-out`, `not-yet-available`, `unknown`) and `in_stock`, true only for `in-stock`. |
| `stock_quantity`, `available_from` | The stock count as Thomann sends it, and the expected ISO date for `on-date` and `not-yet-available` products. See the notes below. |
| `rating`, `review_count` | Star rating out of 5 (Thomann scores out of 10, this is half) and the review count. **Both null when the product has no reviews.** |
| `sales_rank` | Thomann's own sales rank of the product (1 is its best seller). |
| `is_b_stock`, `is_download`, `category_keys` | B-stock (open-box, returned or demo) flag, download flag, and Thomann's category keys. |
| `image_url` | The full-size product photo. Null when the product has none. |

Example row (real, 2026-10-15):

```json
{
  "query": "sennheiser wireless", "position": 1, "product_id": "519215",
  "name": "Sennheiser EW-D SK Base Q1-6", "brand": "Sennheiser",
  "model": "EW-D SK Base Q1-6", "subtitle": "Digital Wireless System",
  "url": "https://www.thomannmusic.com/en-de/sennheiser_ew_d_sk_base_q1_6.htm",
  "image_url": "https://images.static-thomann.de/pics/prod/519215.jpg",
  "currency": "EUR", "price": 499.0, "original_price": 529.0, "retail_price": 599.0,
  "on_sale": true, "discount_percent": 5.7, "discount_amount": 30.0,
  "retail_discount_percent": 16.7, "price_includes_vat": true, "vat_rate": 19.0,
  "unit_price": null, "unit_price_unit": null,
  "availability_text": "In stock", "availability_status": "in-stock", "in_stock": true,
  "stock_quantity": 120, "available_from": null,
  "rating": 4.81, "review_count": 26, "sales_rank": 155,
  "is_b_stock": false, "is_download": false, "category_keys": ["MIWIGI"]
}
```

#### Honest nulls

A field Thomann did not send is `null`, never `0`. Most products have no
`original_price` (only reduced ones do) and many have no `retail_price`. A product
with no reviews has a null `rating` and `review_count`: Thomann sends a count of 0
and no rating, and the actor reports both as absent rather than as a rating of 0.
A product with no photo has a null `image_url`. `stock_quantity` is passed through
as sent: it is 0 on most products that are not in stock and on in-stock products
that are not counted (gift certificates), but not on all (a `short-term` guitar
carried 146), so use `in_stock` for the yes/no.

### 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.0014** per product row written (`product-scraped`), i.e. $1.40 per
  1,000 products. A row that is not written is not charged.

### How it fetches

It reads the product list Thomann embeds in its own search page
(`search_dir.html?sw=<term>&ls=100&pg=<n>&oa=<sort>`), 100 products per request.
Measured 2026-10-15 from a Japan exit: HTTP 200, no cookie, no key. A refusal
(403, 429, 5xx), a 200 that is not a Thomann page, or a page of another store is
treated as the address being blocked, and the actor rotates to a fresh one and
retries.

Things worth knowing, all measured:

- **No depth cap.** Thomann serves any page that exists (page 277 of 277 for a
  27,636-match search), so a term is bounded by your `maxItems` (10,000 at most),
  not by the site. A page past the last is a valid empty page, not an error.
- **Page size.** Thomann lists 25, 50 or 100 per page; the actor uses 100.
- **No matches.** A term with no results is a normal empty answer, not an error.
  It is logged and the run continues with the next term.
- **German product names.** Some product names, mostly spare parts and cables, are
  German even on the English store; they are returned as Thomann prints them.
- **Not offered.** Thomann's filters (manufacturer, price range, features) exist on
  the site but are not wired into this actor; put the brand in the search term.

# Actor input Schema

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

What you would type into the Thomann search box, one term per line (for example shure sm58, boss ds-1 or fender player ii stratocaster). Each term is searched and read separately. A product that two terms both find is returned once, under the first. A brand or category word (fender, stratocaster) may be redirected by Thomann to its brand or category page, which is read as the product list; a broad category word with no product list (guitar) is searched again as the exact phrase.

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

Read at most this many products for each search term. Thomann serves 100 per page. Up to 10,000 per term are accepted.

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

The order Thomann lists the results in. 'Popularity' is its default. The sort is confirmed from Thomann's own answer on every page, and a term fails (at no charge) rather than return rows in an order that was not asked for.

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

Use Apify's automatic proxy (the default). The actor reads the Germany store (EUR) by its address, so the exit country does not choose the currency; a refused request, or an answer from another store, is retried from a fresh address.

## Actor input object example

```json
{
  "searchTerms": [
    "shure sm58"
  ],
  "maxItems": 100,
  "sort": "popularity",
  "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": [
        "shure sm58"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/thomann-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": ["shure sm58"],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/thomann-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": [
    "shure sm58"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/thomann-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/thomann-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/AAhlbAuB5SXxtARb1/builds/4zVSV2Guaz1pvCAgH/openapi.json
