# druni.es - Prices, Ofertas Flash, Variants & Reviews (`abotapi/druni-es-scraper`) Actor

Scrape Druni beauty, cosmetics and perfume products. Search by keyword, category, Ofertas Flash deals or paste product and listing URLs. Extract prices, original prices, discounts, images, product details, and format or colour variants with individual prices and availability.

- **URL**: https://apify.com/abotapi/druni-es-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 product results

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/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

## Druni Product Scraper

Pull product data from Druni (druni.es), a Spanish beauty and perfumery retailer. Search by keyword, a real Druni catalog category, or the site's own Ofertas Flash deals feed, or paste product and listing links directly. Every record includes the current price, the strike-through original price with computed discount when a product is marked down, and format/color variants with their own price and availability when you ask for full detail.

### Why This Scraper?

- **Was-price and discount, structured, not scraped from a badge.** When a product is marked down, both prices are captured plus the computed discount amount and percentage.
- **Ofertas Flash, first class.** A `specialsOnly` toggle narrows any search or category listing to Druni's own live discount feed -- the site's real, queryable markdown signal, not a guessed category.
- **Format and color variants.** Perfumes, creams and similar products return every size (e.g. 20ML/30ML/50ML/100ML) or shade as its own variant, each with its own price and availability, plus a computed unit price per 100 ml/g where the pack size is unambiguous.
- **Reviews straight from the product page.** Overall rating, review count, and the product's own most recent reviews (author, date, rating, body) -- not a third-party review vendor.
- **Two ways in.** Keyword or category search with real site filters (brand, price range, Ofertas Flash, sort), or paste any product/listing link and continue pagination automatically.
- **Resume & recurring updates.** Turn on Incremental mode to get only NEW, UPDATED, and REAPPEARED products on a scheduled re-run of the same search -- see "Resume & recurring updates" below.
- **Optional export to your apps.** Send results into Notion, Linear, Airtable, or any Apify MCP connector alongside the dataset.

### Data You Get

| Field | Example value |
|---|---|
| productId / sku | `123456` |
| name | `Sample Eau de Parfum` |
| brand | `SAMPLE BRAND` |
| category / categoryPath | `Fragancias Femeninas`, `["Druni", "Perfumes", "Perfumes Mujer", "Fragancias Femeninas"]` |
| url | `https://www.druni.es/sample-eau-de-parfum-sample-brand` |
| price / currency | `39.95`, `EUR` |
| originalPrice / discountAmount / discountPercent | `59.95`, `20.00`, `33.4` |
| isOnSpecial | `true` |
| onlineAvailable | `true` |
| image / images | `["https://www.druni.es/sample/image.jpg"]` |
| description | full product description text |
| variants | `[{"sku": "1234568", "name": "Sample Eau de Parfum 50ML", "size": "50ML", "price": 39.95, "currency": "EUR", "onlineAvailable": true, "unitPrice": 79.9, "unitPriceBasis": "100 ml"}]` |
| rating / reviewCount | `4.2`, `18` |
| reviews.averageRating / reviews.totalReviewCount | `4.2`, `18` |
| reviews.items\[] | `[{"rating": 5, "body": "Sample review text.", "author": "Cliente", "date": "2026-06-05"}]` |

> Sample shape: values above are illustrative placeholders, not from a live product.

**Incremental mode only.** When `incrementalMode` is on, every returned record also carries:

| Field | Description |
|---|---|
| `changeType` | `NEW` | `UPDATED` | `UNCHANGED` | `REAPPEARED` | `EXPIRED` |
| `changedFields` | Top-level fields that changed since last seen; non-empty only for `UPDATED` |
| `firstSeenAt` | When this product was first observed by this monitoring campaign |
| `lastSeenAt` | When this product was last observed |

Change detection is based on price, discount/Ofertas Flash status, unit price, availability, and rating/review count -- these are all real signals a monitoring run should surface as `UPDATED`. One thing is deliberately **excluded** from change detection and will never by itself cause a product to come back as `UPDATED`: the `reviews.items` list. The product page only ever surfaces druni.es's most-recent 10 reviews, so a brand-new review silently pushes an older one out of that window even though nothing about the older review changed -- diffing the list itself would flag that churn as a product change it is not. `reviews.averageRating` / `reviews.totalReviewCount` and the top-level `rating` / `reviewCount` fields ARE included in change detection (measured live: review counts across a sample of real products ranged 2-267, not high enough traffic to tick every run the way a mega-catalogue would, so a rating/count move is treated as real signal, not noise).

### How to Use

**1. Keyword search, first page, with reviews (default):**

```json
{
  "mode": "search",
  "searchTerm": "perfume mujer",
  "maxItems": 20
}
```

**2. Category browse, filtered to a brand and sorted by price, only Ofertas Flash items:**

```json
{
  "mode": "search",
  "category": "Fragancias Femeninas",
  "brands": ["DOLCE & GABBANA"],
  "specialsOnly": true,
  "sortBy": "PRICE_ASC",
  "maxItems": 50,
  "maxPages": 5
}
```

**3. Full product detail (variants + description) and reviews for a pasted product link:**

```json
{
  "mode": "url",
  "urls": ["https://www.druni.es/sample-eau-de-parfum-sample-brand"],
  "fetchDetails": true,
  "fetchReviews": true
}
```

**4. Paste the Ofertas Flash listing and keep paginating forward:**

```json
{
  "mode": "url",
  "urls": ["https://www.druni.es/ofertas-flash"],
  "maxItems": 100,
  "maxPages": 10
}
```

**5. Continue a previous run, collecting only new products:**

```json
{
  "mode": "search",
  "category": "Fragancias Femeninas",
  "resumeFromRunId": "<a previous run id or dataset id>"
}
```

### Input Parameters

| Parameter | Type | Description |
|---|---|---|
| `mode` | string | `search` or `url`. |
| `searchTerm` | string | Keyword (search mode only). |
| `category` | string | A real Druni catalog category, e.g. `Fragancias Femeninas` (search mode only). |
| `specialsOnly` | boolean | Keep only products currently in Druni's Ofertas Flash feed (search mode only). |
| `brands` | array | Exact brand names to keep (search mode only). |
| `minPrice` / `maxPrice` | number | EUR price band to keep (search mode only). |
| `sortBy` | string | `RELEVANCE`, `PRICE_ASC`, or `PRICE_DESC` (search mode only). |
| `urls` | array | Product, category, or Ofertas Flash listing URLs to scrape (url mode only). |
| `fetchDetails` | boolean | Fetch full product detail (breadcrumb category path, description, image gallery, format/color variants). |
| `fetchReviews` | boolean | Fetch the product's own reviews. |
| `maxReviewsPerProduct` | integer | Cap on reviews per product; the product page itself only ever surfaces up to 10. |
| `maxPages` | integer | Cap on result pages per search/category/URL entry; `0` or empty = unlimited (walks every page). |
| `maxItems` | integer | Cap on total products **returned** (pushed to the dataset), not pages scanned; `0` = unlimited. See the cap note under "Resume & recurring updates" below -- a capped incremental run still backfills to the cap with unseen products. |
| `resumeFromRunId` | string | Previous run id or dataset id to resume from -- skips every product already collected and returns only the delta, without re-billing it. For recurring daily monitoring of the same search, use Incremental mode instead. |
| `incrementalMode` | boolean | Daily/recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Resume & recurring updates" below. |
| `stateKey` | string | Incremental mode only. Name this monitoring campaign, or deliberately share state across differently-configured runs. Leave empty to derive one automatically. |
| `emitUnchanged` | boolean | Incremental mode only. Also return products unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. |
| `emitExpired` | boolean | Incremental mode only. Also return products from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows. |
| `proxy` | object | Apify proxy configuration. Works on every plan by default. |
| `mcpConnectors` | array | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | string | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | integer | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output Example

```json
{
  "productId": "123456",
  "sku": "123456",
  "name": "Sample Eau de Parfum",
  "brand": "SAMPLE BRAND",
  "category": "Fragancias Femeninas",
  "url": "https://www.druni.es/sample-eau-de-parfum-sample-brand",
  "price": 39.95,
  "currency": "EUR",
  "originalPrice": 59.95,
  "discountAmount": 20.0,
  "discountPercent": 33.4,
  "isOnSpecial": true,
  "onlineAvailable": true,
  "image": "https://www.druni.es/sample/image.jpg",
  "categoryPath": ["Druni", "Perfumes", "Perfumes Mujer", "Fragancias Femeninas"],
  "description": "Sample product description as shown on the site.",
  "variants": [
    { "sku": "123457", "name": "Sample Eau de Parfum 50ML", "size": "50ML", "price": 39.95, "currency": "EUR", "onlineAvailable": true, "unitPrice": 79.9, "unitPriceBasis": "100 ml" },
    { "sku": "123458", "name": "Sample Eau de Parfum 100ML", "size": "100ML", "price": 64.95, "currency": "EUR", "onlineAvailable": true, "unitPrice": 64.95, "unitPriceBasis": "100 ml" }
  ],
  "rating": 4.2,
  "reviewCount": 18,
  "reviews": {
    "averageRating": 4.2,
    "totalReviewCount": 18,
    "items": [
      { "rating": 5, "body": "Sample review text.", "author": "Cliente", "date": "2026-06-05" }
    ]
  }
}
```

> Sample shape: values above are illustrative placeholders, not from a live product.

### A note on reviews and unit pricing

Reviews come directly from the product page itself, not a third-party review vendor -- the page only ever surfaces its most recent reviews (up to 10), while the overall rating and review count reflect the full history. Druni does not display a per-unit (per-100ml/g) price on the site itself; `unitPrice`/`unitPriceBasis` on each variant are computed by this scraper (price divided by that variant's own pack size) only when the size is a single unambiguous value, and are left empty otherwise rather than guessed.

### Resume & recurring updates

There are two different things here -- pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` / automatic checkpoint recovery |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous large crawl: paste a run ID or dataset ID and this run skips products already collected there, returning only the remaining new products. An automatic same-run checkpoint also protects against platform migrations/Resurrects without any input needed.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same* search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` products by default -- duplicates and unchanged products are suppressed (and not charged). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per search/category/Ofertas Flash/URL and Fetch full product detail/Fetch reviews setup automatically; set `stateKey` to name or deliberately share a monitoring campaign.

**Cap note.** `maxItems` counts products **returned**, not products scanned. On an incremental run that suppresses most products as `UNCHANGED`, the walk keeps going past the suppressed ones to backfill the quota with unseen/changed products -- a capped quiet run does not, by itself, mean the bill drops to near zero, because it keeps looking for something to fill the cap with. Every row it does return is still genuinely new/changed, never a repeat. To see the full cost benefit of a quiet run (near-zero pushed rows when nothing changed), leave `maxItems` at `0` (unlimited) so the walk can reach the natural end of the search instead of stopping at a fixed count.

Scheduled-run example -- same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "searchTerm": "perfume mujer", "incrementalMode": true }
```

→ every product comes back with `"changeType": "NEW"`.

Day 2 (the schedule fires again, identical input):

```json
{ "mode": "search", "searchTerm": "perfume mujer", "incrementalMode": true }
```

→ products whose price/discount/Ofertas Flash status/unit price/availability/rating changed come back as `"changeType": "UPDATED"` with `changedFields` listing what changed, brand-new products come back as `"changeType": "NEW"`, products that vanished and came back come back as `"changeType": "REAPPEARED"` -- and products that are still there, unchanged, are **not** returned at all (suppressed, not charged) unless `emitUnchanged` is on.

### Send results into your apps (MCP connectors)

Optionally pipe results into the apps you already use through Model Context Protocol (MCP) connectors. Authorize a connector once under Apify, Settings, Integrations, then select it in the `mcpConnectors` field. Each connector receives a condensed, human-readable summary per product (name plus key fields), while the complete record always stays in the Apify dataset. For Notion, set `notionParentPageUrl` to the page the item pages should be created under, and use `maxNotifyListings` to cap how many items are exported per connector per run. Supported connectors: Notion, Linear, Airtable, and Apify. Leave the field empty to skip; it never changes the dataset output.

### A note on plans

The default connection works on every Apify plan, including the free tier. A residential proxy connection is optional and only worth turning on for very large or sustained runs.

# Actor input Schema

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

'search' finds products by keyword, by a real druni.es category facet (e.g. 'Fragancias Femeninas'), and/or the site's own Ofertas Flash deals, with real site filters. 'url' scrapes any product, category, or Ofertas Flash listing page URL you paste, walking pagination forward automatically.

## `searchTerm` (type: `string`):

Free-text keyword, e.g. 'perfume mujer' or 'crema hidratante'. Leave empty and use Category and/or Ofertas Flash only below to browse without a keyword.

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

A real druni.es catalog category, exactly as the site's own filter shows it, e.g. 'Fragancias Femeninas', 'Cosmética Corporal', 'Maquillaje'. Combines with the keyword above, or browse a whole category by leaving the keyword empty.

## `specialsOnly` (type: `boolean`):

druni.es's real, queryable discount taxonomy is its 'Ofertas Flash' feed -- verified live. The site's separate 'Promociones' nav entry is an editorial content hub with no product listing behind it, so it is intentionally not exposed here. Turning this on with no keyword/category browses the whole Ofertas Flash feed; combined with a keyword/category, it narrows those results to only the items currently marked down, each carrying the site's own strike-through original price and discount.

## `brands` (type: `array`):

Only return products from these brands, e.g. 'NIVEA', 'OPI', 'DOLCE & GABBANA'. Match the exact brand name as shown on the site. Leave empty for all brands.

## `minPrice` (type: `number`):

Only return products priced at or above this amount, in euros.

## `maxPrice` (type: `number`):

Only return products priced at or below this amount, in euros.

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

druni.es's own sort order, verified live to reorder the full server-side result set (so it stays correct across pagination, not just within one fetched page).

## `urls` (type: `array`):

Paste one or more druni.es product pages, category listing pages, or the Ofertas Flash page. Pagination continues forward automatically from any ?p= already in the URL. Filter fields above are ignored in this mode.

## `fetchDetails` (type: `boolean`):

Adds the full breadcrumb category path, complete description, image gallery, and every format/color variant (each with its own price, availability, and a computed unit price per 100 ml/g where the pack size is a single unambiguous value) from the product page. Current price, was-price/discount and card image are already included without this toggle.

## `fetchReviews` (type: `boolean`):

Fetch the product's own reviews (not a third-party vendor): overall rating, total review count, and the page's most recent individual reviews (rating, body, author, date) -- druni.es itself only ever surfaces up to its 10 most recent reviews per product; the rating and total count reflect the full history. Products with no reviews simply return an empty list.

## `maxReviewsPerProduct` (type: `integer`):

Cap on reviews fetched per product when 'Fetch reviews' is on, up to the site's own 10-review page limit.

## `maxPages` (type: `integer`):

Stop after this many result pages per search/category/URL entry. 0 or empty = unlimited (walk every page) -- the run then stops at Max products total.

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

Hard cap on total products returned across every search/category/URL entry. 0 = unlimited (still bounded by Max pages per search).

## `resumeFromRunId` (type: `string`):

Paste a previous run id or dataset id from this account to skip every product it already collected -- this run then returns only new/not-yet-seen products (a delta), never re-billing what was already collected. Leave empty for a normal full run. For recurring daily/weekly monitoring of the SAME search instead, use Incremental mode below.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching products as NEW. Later runs return only NEW, UPDATED and REAPPEARED products by default. Turn on "Emit unchanged products" or "Emit expired products" only when you also want those products returned (and billed). State is kept separately per search/category/Ofertas Flash/URL setup and per Fetch full product detail/Fetch reviews toggle; use State key to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the search/category/Ofertas Flash/URL and Fetch full product detail/Fetch reviews settings -- different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns -- and bills -- extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search/URL set -- not when Max products/Max pages capped it, or when Resume from a previous run was used. This returns -- and bills -- extra synthetic rows, so leave it off unless you need expiry tracking.

## `proxy` (type: `object`):

The default connection works on every Apify plan, including the free tier. A residential connection is optional and only needed for very large or sustained runs.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify -> Settings -> Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON; the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerm": "perfume mujer",
  "specialsOnly": false,
  "brands": [],
  "sortBy": "RELEVANCE",
  "urls": [
    "https://www.druni.es/ofertas-flash"
  ],
  "fetchDetails": false,
  "fetchReviews": true,
  "maxReviewsPerProduct": 10,
  "maxPages": 0,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "mode": "search",
    "searchTerm": "perfume mujer",
    "brands": [],
    "urls": [
        "https://www.druni.es/ofertas-flash"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/druni-es-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",
    "searchTerm": "perfume mujer",
    "brands": [],
    "urls": ["https://www.druni.es/ofertas-flash"],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/druni-es-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",
  "searchTerm": "perfume mujer",
  "brands": [],
  "urls": [
    "https://www.druni.es/ofertas-flash"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/druni-es-scraper --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0mr4fUpmBxFkE997B/builds/Y1W4PmVBuB3xY33u4/openapi.json
