# flaconi Scraper - Perfume & Beauty Price Watch (Germany) (`superslowsloth/flaconi-products`) Actor

flaconi scraper: flaconi.de category listings (perfume, skincare, make-up, hair) to one row per product size with price in EUR, UVP and percent off, 30-day lowest price, price per litre, brand, EAN, badges and fragrance notes. For German beauty price and discount watching.

- **URL**: https://apify.com/superslowsloth/flaconi-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 $2.10 / 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

## flaconi scraper: perfume and cosmetics price and discount watch (Germany)

This flaconi scraper reads the category pages of [flaconi.de](https://www.flaconi.de)
(perfume, skincare, make-up, hair, body, sale) and returns one flat row per
**product size**: the **price in EUR**, the struck-through **UVP** (recommended
retail price) and the **percent off**, the **lowest price of the last 30 days**,
the **price per litre or per piece**, brand, product line, EAN barcode, badges
(`Neu`, `Limitiert`, `+ Geschenk`), voucher code and fragrance notes. Run it on a
schedule and diff the dataset to get a **German beauty price and discount watch**:
which Dior, Chanel, Gucci or Jean Paul Gaultier perfume dropped, which products
sit 40% under the UVP, and what is new.

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

### Input

| Field | Notes |
|---|---|
| `categories` | One or more flaconi categories, one per line: the slug from the page address (`parfum`, `damenparfum`, `make-up`, `sale`) or the page URL (`https://www.flaconi.de/kategorie/parfum/`). Only `/kategorie/<slug>/` listings are supported, not brand pages, search or product pages. A slug flaconi does not know fails with a message and costs nothing. |
| `maxItems` | Products to read per category (default 100, up to 1,000). About 50 a page. flaconi serves about 1,000 products per category however many pages its pager claims, so 1,000 is the most a category yields. |
| `proxyConfiguration` | Apify RESIDENTIAL proxy by default. flaconi.de sits behind Cloudflare, which refuses most datacenter addresses. |

Example input:

```json
{
  "categories": ["parfum", "damenparfum", "https://www.flaconi.de/kategorie/sale/"],
  "maxItems": 100
}
```

### Output

Prices are numbers in euros, gross, as the shop shows them; `currency` is `EUR`.
**One row is one size** (variant) of a product: flaconi sells a perfume in several
sizes under one product page and the grid shows one size per tile, so a page can
appear as several rows. `product_id` is the unique key within a category.

| Field | Notes |
|---|---|
| `category`, `position`, `page` | The category slug the row was read from, the place in that category's own list counting from 1 across pages (taken before repeats are dropped), and the 1-based page. flaconi's default order drifts between requests. |
| `product_id`, `config_id`, `ean` | The size's SKU (`80062316-60`), the product's SKU shared by its sizes (`80062316-C`), and the EAN-13 barcode. **`ean` is null on sponsored tiles**, which the page does not list in its structured data. |
| `name`, `brand`, `line`, `product_type` | The name as the tile prints it (`DIOR Sauvage Elixir Parfum`), and its parts: brand `DIOR`, line `Sauvage Elixir`, type `Parfum` (`Eau de Parfum`, `Duftset`, `Haarserum`...). |
| `url`, `image_url` | The product link with `?variant=<product_id>` (opens that size) and the tile photo. `image_url` is null for a tile with a placeholder (7 of 1,000 `parfum` rows). |
| `price`, `price_from` | The price now for the size in `size`. **`price_from` is true when the tile prints "ab" ("from")**: flaconi has other sizes of the product and this is the cheapest price the tile stands for; `size` and `base_price` stay those of the tile's variant. |
| `original_price`, `on_sale`, `discount_percent` | The "UVP" (unverbindliche Preisempfehlung) the tile strikes through, whether it is above `price`, and `(1 - price / original_price) * 100` to one decimal. **Null when the tile shows no UVP.** The UVP is the manufacturer's recommended price, not a price history. The tile prints "(-31%)" on only some discounted tiles (194 of 233 measured); the printed integer matched the computed figure within one point every time, so the column is computed for all of them. |
| `lowest_price_30d` | The lowest price of the last 30 days, the figure EU law makes the shop disclose beside a reduction ("Niedrigster Preis in den letzten 30 Tagen"), from the search record the page embeds. **Null when flaconi sends none** (208 of 2,000 rows on `sale` and `damenparfum`). It can be above `price` (73 of 1,792): on an "ab" tile it belongs to the product's headline price, not necessarily the size's. |
| `voucher_code` | The code of a campaign the tile advertises as a badge (the badge `CALVINKLEIN` is the code `CALVINKLEIN`). **`price` does not include the voucher's saving.** Null when there is no campaign (56 of 2,000 rows had one). |
| `size`, `base_price`, `base_unit` | The size the tile prices (`60 ml`, `3 Stk`), the price per base unit the tile prints (`1.853,50 € / 1 l` gives `1853.5`) and the unit (`1 l`, `1 Stk`, `1 kg`). |
| `badges`, `sponsored` | The badge texts on the tile (`Neu`, `Limitiert`, `+ Geschenk`, `+ Try me`, `Exklusiv`, a campaign code); empty list when none. `sponsored` is true on a tile flaconi marks "Gesponsert" (a paid placement; 47 of the first 1,000 `parfum` rows). |
| `top_notes`, `heart_notes`, `base_notes` | Fragrance notes as flaconi writes them (`Kopfnoten`, `Herznoten`, `Basisnoten`). Null for a non-fragrance product, a product with none listed, and sponsored tiles. |
| `currency` | `EUR`. |

Example row (real values, 2026-11-05, `parfum` category):

```json
{
  "category": "parfum",
  "position": 1,
  "page": 1,
  "product_id": "80062316-60",
  "config_id": "80062316-C",
  "ean": "3348901567572",
  "name": "DIOR Sauvage Elixir Parfum",
  "brand": "DIOR",
  "line": "Sauvage Elixir",
  "product_type": "Parfum",
  "url": "https://www.flaconi.de/parfum/dior/sauvage/dior-sauvage-elixir-parfum.html?variant=80062316-60",
  "image_url": "https://cdn.flaconi.net/media/catalog/product/3/3/3348901567572-1_c_live.jpg?r=1WAIEA&c=de",
  "currency": "EUR",
  "price": 111.21,
  "price_from": true,
  "original_price": 184.0,
  "on_sale": true,
  "discount_percent": 39.6,
  "lowest_price_30d": 111.21,
  "voucher_code": null,
  "size": "60 ml",
  "base_price": 1853.5,
  "base_unit": "1 l",
  "badges": [],
  "sponsored": false,
  "top_notes": "Grapefruit",
  "heart_notes": "Lavendel",
  "base_notes": null
}
```

#### Honest nulls, and what the listing does not carry

A field the tile did not show is `null`, never `0` or `""`; `badges` is an empty
list when there are none.

- **No rating or review count.** The category grid prints neither.
- **No stock column.** All 365 search records measured on 2026-11-05 said in stock,
  and no tile showed an out-of-stock state, so there is nothing to report.
- **A price is not a price history.** `original_price` is the manufacturer's UVP and
  `lowest_price_30d` covers 30 days. Keep your own history by running on a schedule.
- **Other sizes.** An "ab" tile (`price_from` true) stands for several sizes; the
  others appear as their own rows only if the category lists them as their own tiles.
- **Repeats.** Consecutive pages share 4 to 5 products and the same product can
  appear on two pages; a `product_id` is returned and charged once per category.
  Reading 1,000 `parfum` products took 21 pages and skipped 128 repeated tiles.

### 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.0021** per product written (`product-scraped`), i.e. $2.10 per 1,000
  products. A row that is not written is not charged: repeats of a product the
  same category already returned, and a category that fails, are free.

### How it fetches

flaconi.de is a server-rendered Next.js shop behind Cloudflare. A category page
(`https://www.flaconi.de/kategorie/parfum/`) is about 1.7 MB of HTML holding 45 to
55 product tiles, a schema.org list of the non-sponsored ones (EAN, fragrance
notes) and the search records the page embeds (30-day lowest price, voucher). This
actor reads that page with a browser's TLS fingerprint through Apify's RESIDENTIAL
proxy and rotates the exit address on every refused attempt. A plain HTTP client is
answered 403 ("Just a moment...").

Things worth knowing, all measured on 2026-11-05:

- **Paging is zero-based `?page=`.** The bare address is page 0; `?page=1` is the
  second page. `?p=`, `?seite=` and `?pageNumber=` are ignored.
- **Depth is about 1,000 products a category, not what the pager says.** `parfum`
  claims 124 pages; the last page with tiles is `?page=20` (21 pages) and `?page=21`
  answers HTTP 200 with flaconi's "no products found" message, which the actor reads
  as the end. A short category stops on its pager (`duftoel`: 2 pages, 79 products).
- **An unknown slug is HTTP 404** and fails that category with a message; the other
  categories still run.
- **Weight.** About 1.7 MB and 2 to 3 seconds a page from the build machine, so a
  30-product run is one request and a 1,000-product category is 21.
- **Proxy.** A probe from the Apify platform read the category page through
  datacenter addresses 3 of 10 times (HTTP 403 the other seven) and through
  RESIDENTIAL ones 10 of 10, so RESIDENTIAL is the default and the log warns when it
  is not selected.

If flaconi changes the page, the run fails with a message instead of returning empty
rows.

# Actor input Schema

## `categories` (type: `array`):

flaconi.de categories to read, one per line: the slug from the page address (parfum, damenparfum, make-up, sale, as in https://www.flaconi.de/kategorie/parfum/) or the page URL itself. Only /kategorie/<slug>/ listings are supported (not brand pages, search or product pages). A slug flaconi does not know fails with a message and costs nothing.

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

Read at most this many products for each category. flaconi lists about 50 per page and serves about 1,000 per category whatever its pager says, so 30 reads one page; up to 1,000 are accepted.

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

Use Apify's RESIDENTIAL proxy group (the default). flaconi.de sits behind Cloudflare: a probe from the Apify platform read the category page through RESIDENTIAL addresses 10 of 10 times and datacenter ones 3 of 10 (HTTP 403 otherwise) when this actor was built. A refused request is retried from a fresh address.

## Actor input object example

```json
{
  "categories": [
    "parfum"
  ],
  "maxItems": 30,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# 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 = {
    "categories": [
        "parfum"
    ],
    "maxItems": 30,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/flaconi-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 = {
    "categories": ["parfum"],
    "maxItems": 30,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

```

## MCP server setup

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