# Druni Perfume Prices Scraper (Spain, Promo & Stock) (`steadfetch/es-perfume-price-extractor`) Actor

Structured Druni (Spain perfumery) product rows as JSON: current price, crossed-out promo price, brand, category, bottle size and stock; one row per size variant. Monitor mode (lastRunKey) flags price changes; unchanged products are not billed. HTTP-only, no login, pay per delivered row.

- **URL**: https://apify.com/steadfetch/es-perfume-price-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 priced product delivereds

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

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

## Spain Perfume Prices Scraper (EAN, Promo, Stock)

Structured, schema-validated prices for the Spain perfumery market: give it public product pages and it returns one row per product (per bottle size for variant products) with the current price, the crossed-out regular price on promotions, the EAN when the page exposes it, bottle size and availability. The storefront offers neither a catalogue download nor an API - this Actor turns the public product pages into comparable data. HTTP only: no browser, no login, no CAPTCHAs.

### What it does

This section summarizes the run contract; the exact row shape is below.

### Example output

- **Input:** product page URLs (or bare slugs) from the storefront, plus optional monitor mode.
- **Output:** one dataset row per priced product:

```json
{
  "retailer": "druni",
  "brand": "SHAKIRA",
  "name": "Rojo Elixir",
  "category": "Perfumes > Fragancias Low Cost > Fragancias Mujer",
  "url": "https://www.druni.es/rojo-elixir-shakira-eau-parfum-mujer",
  "price_eur": 19.95,
  "old_price_eur": null,
  "in_promo": false,
  "size_ml": 80,
  "availability": "in_stock",
  "first_seen_at": "2026-10-26T12:00:00.000Z",
  "last_seen_at": "2026-10-26T12:00:00.000Z",
  "price_changed": false
}
```

- **Variant products** (one fragrance sold in 30/50/100 ml) emit one row per size with each size's own price, so catalogues stay comparable by size.
- **Monitor mode:** set `lastRunKey` to compare against the previous run - `price_changed` flags re-pricings, unchanged products are reported as run notes and billed 0.

### Pricing

**2.50 USD per 1,000 priced products** (`apify-default-dataset-item`), plus the Apify default run-start event (`apify-actor-start`). Pages without a public price and unchanged monitor checks are never pushed, so they are never billed.

**Works on the Apify free plan**: with the 5 USD monthly credit, about **1,999 priced products** can be delivered per month (N = floor((5.00 - 0.00005) / 0.0025)).

Measured cost basis (2026-10-26, local run, 256 MB slice): the example run (2 pages, 4 rows) took 4.0 s wall (about 0.0001 USD of platform compute); at scale the 2 s request pacing dominates - 1,000 priced products are about 435 pages and 25 minutes, roughly 0.03 USD of compute per 1,000 products. The 2.50 USD/1k price keeps a ~80x margin over measured cost. Competitor anchor: the perfumery cluster leads sell at 2.35 USD/1k (20 monthly users) with no Spain-coverage equivalent - see the demand sheet behind this Actor.

### Why input-driven discovery

The storefront renders its category listings client-side (verified 2026-10-26: the static category HTML contains only sponsored cards; `?p=2`, XHR, browser-grade TLS and archived snapshots all show the same). Product pages, in contrast, are fully static. The Actor therefore takes explicit product URLs - where to find them: browse the public category pages in any browser. Each run fetches only the pages you list, which keeps runs fast, cheap and polite (2 s minimum interval between requests).

### Legality & politeness

- Only public catalogue pages: no login, no paywall, no CAPTCHA solving, no personal data.
- robots.txt respected: every request passes a hardcoded allow-list (clean product pages + sitemap endpoints); filtered, sessionized and application paths are structurally unreachable from the code.
- The source's edge rejects non-browser User-Agent strings (HTTP 406, verified), so the client sends a standard browser User-Agent. All requests are polite (>=2 s spacing, retries with backoff) and identified as automated traffic by behaviour; no anti-bot circumvention beyond the User-Agent header is used.
- Source ToS (purchase conditions, checked 2026-10-26) contains no clause forbidding automated reading of public catalogue pages.

### FAQ

**Does it need proxies or a browser?** No. Product pages are static HTML; the Actor is HTTP + Cheerio only.

**Why does my category URL not work?** Category pages are input-driven by design (see above). Pass product page URLs, for example `https://www.druni.es/rojo-elixir-shakira-eau-parfum-mujer` or the bare slug.

**How fresh are the prices?** As fresh as the run: every run fetches the pages again and re-parses the live price.

**What does `availability` mean?** `in_stock`, `out_of_stock`, `preorder` or `unknown`, mapped from the page's structured data.

**Is one row one billed item?** Yes - and only rows with an extracted price are emitted. Empty results are recorded as run notes, free.

### Maintenance

Maintained by Steadfetch and tested daily: a scheduled run with the example input is validated against the output schema every day, and the source is re-checked for structural changes.

This Actor bills per delivered row only. Watchdog-style Actors that bill a check even when nothing changed use the reserved `monitor-check` event instead (see the repo CONTRIBUTING guide); that pattern does not apply here because the source exposes no server-side listing to watch.

### Tested daily

This Actor is enrolled in the Steadfetch daily QA: a scheduled run with the example input is validated against the output schema every day. See the `Tested daily` note on the Apify Store listing.

# Actor input Schema

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

Public product page URLs or bare slugs from the storefront (for example rojo-elixir-shakira-eau-parfum-mujer). Duplicates are removed and values are canonicalized; query strings are dropped because filtered URLs are disallowed by the source's robots.txt.

## `maxProducts` (type: `integer`):

Maximum number of product pages processed per run. Pages without a public price are reported as run notes and never billed.

## `lastRunKey` (type: `string`):

Optional key-value store key. When set, the Actor compares each product with the previous run, adds first_seen_at/last_seen_at/price_changed to every row and only delivers products whose price changed (unchanged checks are billed 0). Leave empty to always deliver every product page.

## `strictSchema` (type: `boolean`):

Fail the run when a row does not match the output schema. Keep enabled: Steadfetch never publishes Actors without output-schema validation.

## Actor input object example

```json
{
  "productUrls": [
    "https://www.druni.es/rojo-elixir-shakira-eau-parfum-mujer",
    "https://www.druni.es/your-devotion-dolce-gabbana-eau-parfum-mujer"
  ],
  "maxProducts": 10,
  "strictSchema": true
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "productUrls": [
        "https://www.druni.es/rojo-elixir-shakira-eau-parfum-mujer",
        "https://www.druni.es/your-devotion-dolce-gabbana-eau-parfum-mujer"
    ],
    "maxProducts": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/es-perfume-price-extractor").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 = {
    "productUrls": [
        "https://www.druni.es/rojo-elixir-shakira-eau-parfum-mujer",
        "https://www.druni.es/your-devotion-dolce-gabbana-eau-parfum-mujer",
    ],
    "maxProducts": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/es-perfume-price-extractor").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 '{
  "productUrls": [
    "https://www.druni.es/rojo-elixir-shakira-eau-parfum-mujer",
    "https://www.druni.es/your-devotion-dolce-gabbana-eau-parfum-mujer"
  ],
  "maxProducts": 10
}' |
apify call steadfetch/es-perfume-price-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/es-perfume-price-extractor"
        }
    }
}
```

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/W56hSoRVuTTSVMTSR/builds/xUIRGRWb6MMPHaajQ/openapi.json
