# L'Etoile (letu.ru) Лэтуаль (`sashaebashu/letu-monitor`) Actor

Read the L'Etoile (letu.ru) beauty catalogue: brand, price, base price and discount, stock, rating and review count for every product — plus alerts when a price moves, an item returns to stock or drops out of the catalogue.

- **URL**: https://apify.com/sashaebashu/letu-monitor.md
- **Developed by:** [Sasha Ebashu](https://apify.com/sashaebashu) (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 $5.00 / 1,000 results

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?

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

## L'Etoile Price & Stock Monitor — letu.ru Catalogue

L'Etoile (ЛЭТУАЛЬ, letu.ru) is one of Russia's largest beauty chains. This Actor reads its
catalogue — brand, name, price, the base price and discount, stock, rating and review count for
every product — and tells you when a price moves, an item comes back into stock, or a product drops
out of the range.

No key, no login. The cheap datacenter proxy is enough.

### One clean JSON call per page

The catalogue runs on Oracle Commerce, and its product grid comes from a single search endpoint that
returns everything in one response: **id, brand, price, base price, discount percent, rating, review
count, stock, instalment and link — 24 products a page**. There is no second request per product and
no HTML to scrape. The discount is right there in the grid, not hidden on a two-megabyte product
page.

**Measured: 2,000 products in 31 seconds** across 28 categories and 339 brands, zero duplicates.

### What you get per product

| | |
|---|---|
| **Identity** | Product id, SKU id, name, brand, category, link |
| **Price** | Current price, base price, discount percent and the shop's own discount note |
| **Stock** | In stock, out of stock, or genuinely unknown |
| **Reputation** | Rating and how many reviews back it |
| **More** | Variant count and lead variant, instalment payment, promo markers, first image |

### Prices are per city

L'Etoile prices by region, and its search API will not run without a city. Leave **City ID** empty
and the shop's own IP lookup picks one — Moscow (id 8113) from most exits — and the run logs which
city it priced for, so you are never shown one region's prices while thinking you asked for another.
Set a specific id to price elsewhere.

### Monitor mode

| `changeType` | Means |
|---|---|
| **`price_drop`** / **`price_rise`** | The price moved, with the old figure, the difference and the percentage |
| **`back_in_stock`** | It was out of stock last run and is available now |
| `out_of_stock` | The reverse |
| `new_reviews` | Customers reviewed it since the last run, with how many |
| **`no_longer_listed`** | The product was in the catalogue last run and is not in this one |
| `new`, `unchanged` | First sighting / nothing moved |

**`no_longer_listed` is raised only after a run that could have found the product** — whole-catalogue
mode on, all categories covered, and nothing cut short. When any of that fails, the run says so and
raises nothing, because a delisting claim from a partial read is a guess.

### Notes & limits

Properties of the source, measured rather than assumed:

- **A single category caps at 10,000 products** (about 417 pages). The catalogue is fully reachable
  through the deep leaf categories, which is why the whole-catalogue sweep reads the deepest
  categories first — the sitemap lists **1,310** of them.
- **The end of a category is the API answering 400 with "page beyond result window."** That exact
  message is the end; any other 400 is a real problem and is reported rather than mistaken for it.
  A page that repeats the previous one is also treated as the end.
- **`page` is an object, not a number** — `{number, size}`, and `number` starts at 1. This is the
  kind of detail that returns an empty catalogue if guessed wrong.
- **The base price is only a discount when it is actually higher than the price paid.** When the two
  are equal there is no saving, and `oldPrice`/`discountPercent` are left empty rather than reported
  as a 0% discount.
- **Availability is the shop's own flag.** Anything that is neither in- nor out-of-stock is reported
  as unknown, not guessed as out of stock.
- The Actor honours your **Maximum cost per run** and stops when the cap is reached.

### Input

| Field | What it does |
|---|---|
| **Categories** | `/browse/…` URLs or bare slugs. Leave empty to take them from the shop's sitemap. |
| **Scan the whole catalogue / Maximum categories** | Read every category — required for the delisting alert. |
| **Pages per category** | 24 products per page. |
| **City ID** | Which region to price for. |
| **Brands / Minimum price / Maximum price / In stock only / Only discounted** | Filters, applied before anything is written. |
| **Monitor mode / Output only changes** | The alert behaviour above. |

# Actor input Schema

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

Category pages to read, for example `https://www.letu.ru/browse/parfyumeriya`. A bare slug like `parfyumeriya` also works. Leave empty to take them from the shop's own category sitemap.

## `scanWholeCatalogue` (type: `boolean`):

Read every category the shop lists in its sitemap — 1,310 of them, including the deep leaf categories that make the full catalogue reachable. Needed for the "no longer listed" alert.

## `maxCategories` (type: `integer`):

How many categories to take from the sitemap. Deepest (leaf) categories are read first.

## `maxPagesPerCategory` (type: `integer`):

Each page holds 24 products. A single category caps at 10,000 products (about 417 pages); leaf categories end far sooner.

## `cityId` (type: `string`):

Prices are per-region. Leave empty and the shop's own IP lookup picks a city (Moscow, id 8113, from most exits). Set a specific id to price for another region.

## `brandFilter` (type: `array`):

Keep only these brands, spelled as the shop spells them — `DIOR`, `VIVIENNE SABO`. Leave empty for all.

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

Keep only products at or above this price. 0 turns it off.

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

Keep only products at or below this price. 0 turns it off.

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

Drop products the shop marks as out of stock.

## `onlyDiscounted` (type: `boolean`):

Keep only products whose current price is below their base price.

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

Stop after this many products.

## `maxConcurrency` (type: `integer`):

4 is comfortable.

## `monitorMode` (type: `boolean`):

Compare against the last run and label what moved: a price up or down with the old figure, a product coming back into stock or leaving it, new reviews, or a product no longer listed.

## `onlyChanges` (type: `boolean`):

With monitor mode on, skip products where nothing moved.

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

The search API answers from most regions over the cheap datacenter group. On by default; switch off only if you have a reason to.

## Actor input object example

```json
{
  "categoryUrls": [
    "https://www.letu.ru/browse/parfyumeriya",
    "https://www.letu.ru/browse/makiyazh"
  ],
  "scanWholeCatalogue": false,
  "maxCategories": 20,
  "maxPagesPerCategory": 25,
  "cityId": "",
  "brandFilter": [],
  "minPrice": 0,
  "maxPrice": 0,
  "inStockOnly": false,
  "onlyDiscounted": false,
  "maxItems": 1000,
  "maxConcurrency": 4,
  "monitorMode": false,
  "onlyChanges": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `alerts` (type: `string`):

No description

## `full` (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 = {
    "categoryUrls": [
        "https://www.letu.ru/browse/parfyumeriya",
        "https://www.letu.ru/browse/makiyazh"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sashaebashu/letu-monitor").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 = { "categoryUrls": [
        "https://www.letu.ru/browse/parfyumeriya",
        "https://www.letu.ru/browse/makiyazh",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sashaebashu/letu-monitor").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 '{
  "categoryUrls": [
    "https://www.letu.ru/browse/parfyumeriya",
    "https://www.letu.ru/browse/makiyazh"
  ]
}' |
apify call sashaebashu/letu-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sashaebashu/letu-monitor"
        }
    }
}
```

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/QcnMLtFKhp80H8jW7/builds/CfERcsdf32AdspgZy/openapi.json
