# Sephora Products Scraper (`nice_dev/sephora-products-scraper`) Actor

Scrape Sephora products (US & Canada) by keyword, brand, category, product ID or URL: prices, ratings, badges, variants, ingredients, reviews, Q\&A, EAN. Export to JSON, CSV or Excel.

- **URL**: https://apify.com/nice_dev/sephora-products-scraper.md
- **Developed by:** [Nice Dev](https://apify.com/nice_dev) (community)
- **Categories:** E-commerce, MCP servers
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.86 / 1,000 products

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

### 🛒 What is Sephora Products Scraper?

**Sephora Products Scraper** extracts **products from [Sephora](https://www.sephora.com)** (United States and Canada): **name, brand, price, rating, reviews count, badges and photos**, and, on request, the **full product page** (every shade and size with its price and stock, ingredients, how to use), the **customer reviews with the reviewer's profile**, the **rating histogram with barcodes (EAN / GTIN)** and the **questions and answers**. It works as a **Sephora API alternative**: run it on a schedule, call it from your code, or plug it into Make, Zapier or n8n.

Type a **keyword** (`mascara`), a **brand**, a **category** or a **product ID**, or paste any Sephora search, category, brand or product URL, click **Start**, and download the products in JSON, CSV or Excel. No login, nothing to set up. A results page holds up to **200 products**, so a whole brand or category comes in a few pages.

### 📋 What data can you extract from Sephora?

One item per product, 68 fields:

| Category | What you get |
| --- | --- |
| 🏷️ **Product** | name, brand, product ID, link, main image — `Lash Idôle Lengthening & Volumizing Mascara`, `Lancôme`, `P467208` |
| 💰 **Price** | lowest and highest price, price as shown, regular price when on sale, currency (USD or CAD) — `$16.00 - $30.00` |
| ⭐ **Ratings** | average rating, number of reviews, number of shades the search card announces — `4.5`, `10,770 reviews` |
| 🎖️ **Badges** | best seller, new, Sephora exclusive, limited edition, limited time offer, online only, app exclusive, Beauty Insider only, sponsored |
| 🚚 **Delivery** | in-store pickup and same-day eligibility, read from the product page (empty on a search result) |
| 📍 **Ranking** | position and results page in the search that found the product, the keyword, brand or category searched |
| 🧴 **Product page** | full description, how to use, ingredients, highlights (clean, vegan, long-wearing…), brand presentation, category path, loves count, all photos and videos |
| 🎨 **Shades and sizes** | every variant with its own price, sale price and stock status, in stock or not, how many units per order |
| 💬 **Review summary** | Sephora's own AI summary of the reviews, strengths and weak points with their counts |
| 📊 **Review statistics** | number of reviews per star, reviewers who recommend or not, helpful votes, first and last review date, **barcodes (EAN / GTIN)** |
| 📝 **Reviews** | rating, title, text, date, recommended or not, helpful votes, photos, brand reply, verified purchase, free sample and staff flags, and the **reviewer profile**: skin type, skin tone, eye and hair color, skin concerns, age range |
| ❓ **Questions and answers** | each question with its answers, the brand's answers flagged |

Every field, with an example, is listed in the **Output** section below.

The product page, the review statistics, the reviews and the questions are **options**, off by default: the search results alone already give the Product, Price, Ratings, Badges and Ranking categories; the Delivery data comes from the product page. Fields marked **details** in the Output tab are filled when **Extract product details** is on; the statistics, the reviews and the questions have their own switch and do not need it.

### ✅ Why use Sephora Products Scraper?

- 🔎 **Search the way you think**: by keyword, brand, category, product ID or pasted URL, several of each in one run, with a cap per search. Not only URLs.
- 🎛️ **Every filter of the site**: price, rating, brands, new arrivals, any other filter of the Sephora sidebar, plus on sale, Sephora exclusives, best sellers, in stock and excluded words.
- 🇺🇸🇨🇦 **United States and Canada**, in English and, for Canada, in **French**: French names and descriptions, prices in CAD, reviews written in French (the questions come in every language).
- 🧴 **The complete product**: every shade and size with its price and stock, ingredients, how to use, highlights, photos and videos.
- 📝 **Reviews with the reviewer's profile**, rating histogram, barcodes (EAN / GTIN) and the customers' questions and answers.
- 🔔 **Monitoring built in**: tick **Only new products**, schedule the Actor, and each run returns (and charges) only the products it has never delivered before.
- 🧩 **Nothing twice**: pagination is automatic, a product found by several searches is saved once.
- 🔌 API, scheduling, monitoring, integrations (Make, Zapier, n8n, Google Sheets…), proxy rotation and JSON/CSV/Excel export via the Apify platform.

### 🚀 How to scrape Sephora

1. Create a free Apify account.
2. Open **Sephora Products Scraper** and type a **Search keyword** (e.g. `mascara`), or fill **Brands**, **Categories** or **Product IDs**.
3. Or paste your own Sephora URLs into **Start URLs**: any search, category or brand page (all filters set on the site are kept, pagination is automatic) or single product pages.
4. Pick the **Country** (and **Language** for Canada), tick the options you need (product details, reviews, questions), set **Max products** (100 by default, 0 = no limit) — and **Max products per search** when you run several searches — then click **Start**.
5. Download the dataset in JSON, CSV, Excel or via API.

### 💰 How much does it cost to scrape Sephora?

This Actor uses **pay per event** pricing: you pay for each product saved, plus a small fee at each run start. The options are **off by default** and each has its own price: you pay for them only when you tick them, and only for what the Actor delivers. Prices are in US dollars **per 1,000 events** and depend on your Apify plan; a paid plan pays less. Platform usage (compute, proxy) is included in the price.

| You pay for | Event | Free | Bronze | Silver | Gold |
| --- | --- | --- | --- | --- | --- |
| Each product saved (the row from the search, without the options below) | `apify-default-dataset-item` | **$0.89** | $0.88 | $0.87 | $0.86 |
| The full product page of a product (**Extract product details**), added to the product | `product-details` | **$0.49** | $0.39 | $0.29 | $0.19 |
| The rating histogram and barcodes of a product, once per product (**Add rating histogram and barcodes (EAN)**; also added when **Include reviews** is on) | `review-stats` | **$1.19** | $1.18 | $1.17 | $1.16 |
| Each review delivered (**Include reviews**) | `review` | **$0.039** | $0.034 | $0.026 | $0.019 |
| Each question delivered (**Include questions & answers**) | `question` | **$8.09** | $8.08 | $8.07 | $8.06 |
| A product page that the standard connection could not read and that was read on a premium connection of ours: only for the pages read that way, never with your own proxies | `residential-fallback` | **$2.52** | $2.51 | $2.50 | $2.49 |
| Each product a filter of the Actor drops after its search result was read (a product you keep pays its own price, never both) | `filter-check` | **$0.039** | $0.034 | $0.029 | $0.024 |
| Each product a filter of the Actor drops after its product page was read | `detail-filter-check` | **$0.32** | $0.31 | $0.30 | $0.29 |

Plus **$0.01 per run start** (`apify-actor-start`), on every plan.

Examples, run start included:

- **A search of 200 products, no option**: $0.19 on Free ($0.178 for the products + $0.01 run start = $0.188), $0.18 on Gold ($0.172 + $0.01 = $0.182).
- **A daily follow-up of 1,000 products with their full product page, once a day for 30 days** (30,000 products): $1.39 a day on Free ($0.89 + $0.49 for the pages + $0.01 run start), **$41.70 for the 30 days**; $1.06 a day on Gold ($0.86 + $0.19 + $0.01), **$31.80 for the 30 days**.

A product ID or a product URL always comes with its full product page: it is charged as **Extract product details**. A review or a question you do not ask for (**Max reviews per product**, **Max questions per product**, the star and date limits) is never delivered, so never charged. Products already delivered (**Only new products**) are never charged again.

The filters the Actor applies itself (on sale, Sephora exclusives, best sellers, in stock, excluded words) are checked on every product it reads: a product you keep costs its normal price, a product they drop is not saved, does not count in **Max products** and is charged a small filter check, never more than a product you keep. The price, rating, brand, new arrivals and site filters are applied by Sephora itself to its search and cost nothing more.

If your filters match almost nothing in one search (300 products in a row dropped after their product page was read), that search stops and the run goes on with the next ones; the run stops only when every search is in that case.

### ⚙️ Input

Search a keyword and save the first 200 products:

```json
{
    "query": "mascara",
    "maxItems": 200
}
```

One product in the Canadian store, in French, with its full page, its newest 50 reviews and 10 questions:

```json
{
    "productIds": ["P467208"],
    "country": "CA",
    "language": "fr",
    "extractDetails": true,
    "includeReviews": true,
    "maxReviewsPerProduct": 50,
    "reviewsSort": "newest",
    "includeQuestions": true,
    "maxQuestionsPerProduct": 10
}
```

Two whole brands, best sellers first, from 10 to 40 dollars, rated 4 stars and up, 50 products per brand, and only the products not delivered before:

```json
{
    "brands": ["Lancôme", "Rare Beauty"],
    "sortBy": "bestSelling",
    "minPrice": 10,
    "maxPrice": 40,
    "minRating": "4",
    "excludeKeywords": ["mini"],
    "maxItemsPerQuery": 50,
    "onlyNew": true,
    "stateKey": "brands-top"
}
```

| Field | Notes |
| --- | --- |
| `startUrls` | Sephora pages: a search (`https://www.sephora.com/search?keyword=mascara`), a category (`https://www.sephora.com/shop/face-serum`), a brand (`https://www.sephora.com/brand/lancome`) or a product (`https://www.sephora.com/product/lancome-lash-idole-lash-lifting-volumizing-mascara-P467208`). Filters set on the site are kept, pagination is automatic. A price bound or a sort already in the URL wins over the form's (a warning says so); a price bound the URL lacks is completed by **Min price** / **Max price**. A Canadian URL (`https://www.sephora.com/ca/fr/shop/mascara`) switches the country. When filled, the keyword, brand, category and product ID fields are ignored (filters, caps, options and monitoring still apply). Max 1,000 URLs. |
| `query`, `searchQueries` | Free-text search, exactly as typed in the Sephora search box (`mascara`, `vitamin c serum`); `searchQueries` adds more keywords (one search each, up to 100). |
| `brands` | Whole brand catalogues: the brand name or the slug of its Sephora page (`lancome`, `Rare Beauty`, `the-ordinary`). One search per brand. |
| `categories` | Whole categories: the slug of a Sephora category page (`mascara`, `face-serum`, `perfume`) or a category id (`cat140006`). One search per category. |
| `productIds` | Sephora product ids (`P467208`, at the end of every product URL), up to 1,000. Each one is read from its product page; with `maxItems` below the list, the first ids of the list are the ones read. |
| `country` | The store to read: `US` (prices in USD) or `CA` (prices in CAD, Canadian assortment). |
| `language` | `en` or `fr`. French is only available with Canada: names and descriptions in French, and only the reviews written in French (the questions are all of them, in any language). |
| `maxItems` | Stop after this many products for the whole run (`0` = unlimited). |
| `maxItemsPerQuery` | Cap for EACH search (keyword, brand, category or URL). `0` = no per-search cap. |
| `sortBy` | Order of the results, as on the site: `relevance`, `bestSelling`, `topRated`, `priceLowToHigh`, `priceHighToLow`. With the caps it decides which products you get. |
| `extractDetails` | Open each product page: every shade and size, ingredients, how to use, highlights, photos, videos, category path (option, off by default). |
| `includeReviewStats` | Add the number of reviews per star, the reviewers who recommend and the barcodes (option, off by default). |
| `includeReviews` | Add the customers' reviews with the reviewer's profile (option, off by default). |
| `maxReviewsPerProduct` | Reviews to add per product, 0 = all of them, up to about 9,000 reviews per product with 1 GB of memory or more, about 4,000 at the default 512 MB: a dataset row holds at most 7 MB, and the Actor fits it to the run's memory, and the message at the end of the run counts the products whose reviews or questions were cut (`20`, `100`). |
| `reviewsSort` | Which reviews come first: `newest`, `oldest`, `mostHelpful`, `highestRating`, `lowestRating`. |
| `reviewsPostedAfter` | Only reviews from a date (`2026-09-01`) or a period before now (`7 days`, `1 month`). With the newest first, the Actor stops at the first older review. |
| `reviewsMinRating`, `reviewsMaxRating` | Only reviews with at least or at most this many stars, from 1 to 5 (`2` with the maximum gives the negative reviews). |
| `includeQuestions`, `maxQuestionsPerProduct` | Add the customers' questions with their answers, newest first; 0 questions = all of them (option, off by default). |
| `minPrice`, `maxPrice` | Price bounds in the store currency, applied by Sephora. On a pasted search URL that already has the same bound, the URL's bound is kept and a warning says so. |
| `minRating` | Only products rated at least this many stars (`1`, `2`, `3`, `4`), applied by Sephora. |
| `filterBrands` | Keep only these brands inside a keyword or category search (`Lancôme`, `Rare Beauty`), applied by Sephora. |
| `newArrivalsOnly` | Only the products Sephora marks as new, applied by Sephora. |
| `refinements` | Any other filter of the Sephora sidebar, as the site writes it in the URL (`filters[SkinType]=oily`, `filters[benefits]=longWearing`). A filter the site does not know stops the run with a clear message instead of returning nothing. |
| `saleOnly`, `exclusiveOnly`, `bestsellersOnly` | Only the products on sale, sold exclusively at Sephora, or flagged best sellers. Applied by the Actor on every product it reads, except the best seller badge on a product given by ID or URL: Sephora shows that badge on search results only, so those products are kept (a warning says so). |
| `inStockOnly` | Drop the products whose every shade and size is out of stock. Needs `extractDetails`: stock is on the product page. |
| `excludeKeywords` | Drop the products whose name or brand contains one of these words (`mini`, `travel`; case and accents ignored). |
| `onlyNew`, `stateKey`, `resetState` | Monitoring: only the products never delivered under this memory key (`new-skincare`); `resetState` forgets the memory. |
| Advanced | `proxyConfiguration` (Apify proxy by default, included in the price; the Apify residential proxy is not available), `maxConcurrency`, `maxRequestsPerMinute`, `minRequestIntervalMs`, `maxRequestRetries`, `debugLog`. |

### 📦 Output

One product, shortened (the texts, the photos and the variants are cut; the real item keeps them whole):

```json
{
    "id": "P467208",
    "url": "https://www.sephora.com/product/lancome-lash-idole-lash-lifting-volumizing-mascara-P467208",
    "name": "Lash Idôle Lengthening & Volumizing Mascara",
    "brand": "Lancôme",
    "skuId": "2417145",
    "price": 16,
    "priceMax": 30,
    "priceText": "$16.00 - $30.00",
    "listPrice": null,
    "onSale": false,
    "currency": "USD",
    "country": "US",
    "rating": 4.5001,
    "reviewsCount": 10770,
    "image": "https://www.sephora.com/productimages/sku/s2417145-main-zoom.jpg",
    "moreColors": 2,
    "isBestseller": true,
    "isNew": false,
    "isSephoraExclusive": false,
    "sponsored": false,
    "position": 1,
    "page": 1,
    "searchTerm": "mascara",
    "searchUrl": "https://www.sephora.com/search?keyword=mascara",
    "description": "What it is: A volumizing and lengthening mascara that targets every single lash for instant lift, length, and fanned-out volume without clumps…",
    "ingredients": "AQUA / WATER / EAU, PROPYLENE GLYCOL, STYRENE/ACRYLATES/AMMONIUM METHACRYLATE COPOLYMER, …",
    "highlights": ["Volumizing", "Long-wearing", "Lengthening", "Without Parabens", "Oil Free", "Without Silicones"],
    "lovesCount": 275951,
    "categoryPath": ["Makeup", "Eye", "Mascara"],
    "variationType": "Size",
    "variantsCount": 3,
    "inStock": true,
    "variants": [
        {
            "skuId": "2417145",
            "name": "0.27 fl oz/8ml",
            "size": "0.27 fl oz/8ml",
            "price": 30,
            "listPrice": null,
            "inStock": true
        }
    ],
    "reviewsSummary": "Customers often note no clumps, lengthening, and separation as favorable aspects. …",
    "reviewsPros": [{ "label": "No Clumps", "count": 62 }],
    "ratingHistogram": { "1": 164, "2": 276, "3": 786, "4": 2328, "5": 7216 },
    "recommendedCount": 9728,
    "firstReviewAt": "2020-12-22T14:41:42.000Z",
    "lastReviewAt": "2026-09-28T17:20:24.000Z",
    "gtins": ["3614273066365", "3614274306477"],
    "reviews": [
        {
            "id": "406522608",
            "rating": 3,
            "title": "Wanted to love it so bad",
            "submittedAt": "2026-09-28T17:20:24.000Z",
            "author": "jaymocz",
            "isRecommended": false,
            "isVerifiedPurchase": true,
            "skinType": null,
            "ageRange": null
        }
    ],
    "questions": [
        {
            "id": "8572736",
            "text": "Best mascara for short ,fine lashes and that is anti aging .",
            "submittedAt": "2026-08-18T17:12:36.000Z",
            "answersCount": 0,
            "answers": []
        }
    ],
    "scrapedAt": "2026-10-02T12:00:00.000Z"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV or Excel.

#### All 68 fields

A field of an option you did not tick is `null`, never missing. A product read by its ID or its URL has no search card: what only a card says (`moreColors`, the badges `isBestseller` and `sponsored`, `position`, `page`, `searchTerm`, `searchUrl`) is `null` for it, which is why the best sellers filter does not judge it.

| Fields | What you get |
| --- | --- |
| `id`, `url`, `name`, `brand`, `skuId` | `P467208` (the same id in the US and the Canadian store: the same product in the two stores is two rows, told apart by `country`), the product page, `Lash Idôle Lengthening & Volumizing Mascara`, `Lancôme`, the SKU Sephora shows first (the shade / size its page opens on; with several sizes, `price` is the lowest of them, not always this one) |
| `price`, `priceMax`, `priceText`, `listPrice`, `onSale`, `currency`, `country` | `16`, `30`, `$16.00 - $30.00`, the regular price when on sale, `false`, `USD` or `CAD`, `US` or `CA`. From a search alone, when only some sizes are on sale, the highest price and the regular price are read from Sephora's price range; the product page (with details) gives them exactly |
| `rating`, `reviewsCount`, `image`, `moreColors` | `4.5001`, `10770`, the main photo, the number of shades the search card announces (Sephora's own count: it can be one below `variantsCount`) |
| `isBestseller`, `isNew`, `isSephoraExclusive`, `isLimitedEdition`, `isLimitedTimeOffer`, `isOnlineOnly`, `isAppExclusive`, `isBeautyInsiderOnly`, `sponsored` | the badges: `true` or `false` |
| `pickupEligible`, `sameDayEligible`, `nextDayEligible`, `shipToHomeEligible` | the delivery modes. Sephora sets them for a store and a postcode the visitor chooses, and a search card says `false` to everyone, so a card gives `null` (unknown), never `false`. `pickupEligible` and `sameDayEligible` come from the product page (the shade it opens on, with `extractDetails` or a product read by its ID or URL) and say what that page says without a chosen store; `nextDayEligible` and `shipToHomeEligible` are not on the page: always `null` |
| `position`, `page`, `searchTerm`, `searchUrl` | rank in its search (1 = first), results page (200 products per page), the keyword, brand, category or URL that found it, the equivalent Sephora page |
| `description`, `shortDescription`, `howToUse`, `ingredients` | **details**: plain texts, ingredients of the default shade or size |
| `highlights`, `lovesCount` | **details**: `Clean at Sephora`, `Vegan`…, number of shoppers who loved it |
| `brandId`, `brandUrl`, `brandDescription` | **details**: the brand's id, page and presentation |
| `categoryPath`, `categoryId`, `categoryUrl` | **details**: `Makeup`, `Eye`, `Mascara`, the category id and page |
| `variationType`, `variantsCount`, `inStock`, `variants` | **details**: `Size` or `Color`, number of shades or sizes, in stock, every shade or size with SKU, price, regular price, stock, photo and link |
| `images`, `videos` | **details**: all photos of the default shade (the card's photos without details), product videos |
| `reviewsSummary`, `reviewsPros`, `reviewsCons` | **details**: Sephora's AI summary, strengths and weak points with their counts |
| `isReturnable`, `maxPurchaseQuantity` | **details**: can be returned, most units per order |
| `ratingHistogram`, `recommendedCount`, `notRecommendedCount`, `ratingsOnlyCount` | **review statistics**: reviews per star (1 to 5), reviewers who recommend or not, ratings without text |
| `helpfulVotesCount`, `notHelpfulVotesCount`, `firstReviewAt`, `lastReviewAt`, `gtins` | **review statistics**: votes on the reviews, date of the first and the last review, barcodes (EAN / GTIN) |
| `reviews` | **reviews**: id, rating, title, text, date, author, author tier (Insider, VIB, Rouge), recommended or not, helpful votes, verified purchase, free sample, staff, photos, brand reply (rarely filled), language, skin type, skin tone, eye color, hair color, skin concerns, age range (the profile as the site shows it: `Combination`, `30's`) |
| `questions` | **questions**: id, text, date, author, number of answers and the answers, up to 20 per question (text, date, author, brand answer or not, votes); the number of answers is the site's total, so it can be above 20 |
| `scrapedAt` | ISO timestamp of the read |

### 💡 Tips

#### How to get more results

Set `maxItems` to `0` and use a broad keyword, a brand or a category: a search returns every product the site lists for it (up to 20,000 per search). Add `sortBy` and `maxItemsPerQuery` to take only the top of each search (the 50 best sellers of a category, for example).

#### How to reduce costs

The price is per product, so the levers are `maxItems`, `maxItemsPerQuery`, the filters and `onlyNew` for recurring runs (you never pay twice for the same product). The filters Sephora applies itself (price, rating, brands, new arrivals, `refinements`) make the search return fewer products to begin with. Tick the options only for what you need, and cap the reviews and the questions with `maxReviewsPerProduct` and `maxQuestionsPerProduct`: they are charged one by one.

#### Several searches in one run

Fill `searchQueries`, `brands` and / or `categories`: the Actor runs one search per keyword, brand and category (up to 100 of each). The single `query` still works and is added to the list. A product found by several searches is saved, and charged, once. Set `maxItemsPerQuery` to give every search its own cap: without it the first searches can use up the whole `maxItems` budget. You can also paste several search URLs into `startUrls`: each one is a search of its own, with the same cap.

#### Track the ranking of a brand

Every product found by a search carries its `position` and `page` in that search, with the sort you chose. With **Extract product details** the rows are saved as their product pages arrive, not in the site's order: sort them by `position`. Run the same keyword or category every day with `sortBy` set to `bestSelling` and compare the positions of a brand over time.

#### Canada in French

Choose `CA` for **Country** and `fr` for **Language**: names and descriptions come in French, prices in CAD, and the reviews are those written in French (there are fewer of them than in English). The questions and answers are not filtered by language: you get all of them, each in the language it was written in. A Canadian URL pasted into `startUrls` switches the country by itself.

#### Monitoring: only the new products

Tick **Only new products** (`onlyNew`) and schedule the Actor. The first run returns everything; each later run skips the products already delivered: they are not saved, not charged, and their product page is not even opened. The memory lives in a named key-value store of your account (`sephora-products-scraper-seen`, up to 150,000 products per key) and is only updated with products that really reached the dataset, so a failed run never hides anything. A product is remembered by its id and its store: the same product in the United States and in Canada counts twice. Give each schedule its own `stateKey` (two schedules sharing a key would hide each other's products), and tick `resetState` once to start over. A product that your filters drop is checked (and charged the small filter check) once: the next runs with the same filters skip it for free, unread. Change a filter or a search, or tick `resetState`, to check those products again (the limits, the speed and the proxy are not filters). Sephora has no "newest first" sort, so a run reads a search from the top until it has seen 20 results pages in a row (about 4,000 products) where every product was already delivered by an earlier run: it then ends that search. It is quick, since a results page holds 200 products. What it means for you: a new product that Sephora lists further down than 20 pages of products you already have is not found by that run. To keep new products near the top, tick **New arrivals only** (`newArrivalsOnly`, applied by Sephora) or use a narrower brand, category or filter, so that the search holds few products and the new ones come first. The first run, and a search of fewer than 20 pages, are always read to the end.

### 🔌 Integrations and API

Call the Actor via the Apify API, the JavaScript or Python clients, or connect it with integrations and webhooks (Make, Zapier, n8n, Google Sheets, Slack, Airtable…). The dataset can be fetched as JSON or CSV from any tool.

### 🤖 Use with AI agents (MCP)

AI agents (Claude, ChatGPT, Cursor…) can find and run this Actor through the [Apify MCP server](https://mcp.apify.com), billed to their Apify account like any run. It returns one item per Sephora product. Actor id: `nice_dev/sephora-products-scraper`; MCP server with this Actor only: `https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/sephora-products-scraper`.

Smallest input, for a cheap first call:

```json
{
    "query": "mascara",
    "maxItems": 10
}
```

Key output fields: `url`, `name`, `brand`, `price`, `currency`, `rating`, `reviewsCount` and, with the options, `ingredients`, `variants`, `reviews` and `questions`.

Cost: $0.89 per 1,000 products saved plus $0.01 per run start ($0.86 per 1,000 on the Gold plan); the options you tick (product details $0.49 per 1,000 products, $0.19 on Gold; review statistics, reviews, questions) and the Actor's own filters cost extra, see the pricing section above and the **Pricing** tab. Cap each call with `maxItems` and, through the API, with the run option `maxTotalChargeUsd`.

### ❓ FAQ

#### Is it legal to scrape Sephora?

The Actor only reads what Sephora shows publicly to any anonymous visitor. It logs in to nothing. Reviews and questions carry the nickname their author chose to show, and a review can carry a profile the author filled in (skin type, age range…): this is personal data protected by GDPR, so do not store or use it without a legitimate reason. You are responsible for using the data in compliance with Sephora's Terms of Use and applicable law. This Actor is not affiliated with, endorsed by or sponsored by Sephora.

#### Does it need a login or a proxy?

No login. The proxy is included in the price: leave the default setting (the Apify residential proxy is not available). A request the site turns away is retried at once on a new proxy session, up to 10 times on top of the retries (without a proxy, after a pause of 5 seconds, doubled at each retry up to 150 seconds). A product page that keeps being turned away on the standard connection is read on a premium connection of ours instead (event `residential-fallback`, charged only for the pages read that way); with your own proxies, your proxy is used as it is.

#### Is the data safe to open in Excel or to show on a web page?

Names, descriptions and above all the customers' reviews and questions are written by people, and copied as they are (only the HTML tags are dropped from the reviews and the questions). A text can begin with `-`, `+`, `=` or `@`: Excel and Google Sheets may read such a cell of a CSV file as a formula or as a number. The Actor leaves the text as it is, so that the JSON and the API give the real value: when you open a CSV, import these columns as text. On a web page, escape every field like any text written by a stranger.

#### Known limitations

- Only the United States and Canadian stores are covered; the other Sephora sites (France, United Kingdom, Mexico…) are not.
- A product has no date: there are no date filters for products, and `onlyNew` reads each search until 20 results pages in a row (about 4,000 products) hold nothing new, so a new product listed further down is not found (see *Monitoring: only the new products*). Reviews have a date and can be filtered with `reviewsPostedAfter`.
- `description`, `ingredients`, `variants` and the other **details** fields come from the product page: they are `null` when **Extract product details** is off. The statistics, the reviews and the questions have their own switches.
- `inStock` tells whether a shade or a size can be ordered online; the stock of each store is not offered.
- The reviewer's profile (skin type, age range…) is only what the reviewer chose to fill in: it is `null` otherwise. It is written as the site shows it (`Combination`, `Fair Light`, `30's`, `50's +`, `Acne/Blemishes`).
- The brand's reply to a review (`brandResponse`) is rarely filled on Sephora: it is `null` on almost every review.
- A question comes with up to 20 of its answers (Bazaarvoice's limit); `answersCount` gives the total the site has.
- Sephora's own search is forgiving: a misspelled or meaningless word still returns the closest products (about 100), it never returns nothing. Narrow it with `filterBrands` and `excludeKeywords`.
- A brand or a category page that Sephora does not have (a misspelt slug: the page of the makeup category is `makeup-cosmetics`, not `makeup`) is not an error: the Actor warns, reads nothing for it and goes on with the other searches. If nothing else gave a product, the run ends *Succeeded* and says which slug was not found, with the address to check (`sephora.com/shop/<slug>` for a category, `sephora.com/brand/<slug>` for a brand; `/ca/en/` or `/ca/fr/` after `sephora.com` for the Canadian stores). A category with no filter set that returns 0 products is treated the same way. Filters of the form or of a pasted URL that match nothing give an empty search (for a brand that exists too), and a `refinements` entry that the site does not know still ends the run with an error that names it: when a price or another filter could be the cause as well, the Actor asks Sephora once for that entry alone, and a valid one is never blamed.
- Sephora sends some keywords to a page instead of a search (typing `gift sets` opens its Gifts category). When that page is a category or a brand, the Actor reads it, in the same store and with the same filters, sort and limits as the keyword, and the products carry the keyword's search. When it is another page (`gift card` opens the gift cards page) there is no list of products to read: the Actor warns with the page it leads to, reads nothing for that keyword and goes on with the other searches; if nothing else gave a product the run ends *Succeeded* and says so. To read such a page, write the slug of its category in `categories`.
- `onlyNew` remembers product ids, not their content: a product whose price changed is not returned again.
- A product that Sephora's results show twice is saved once. It happens when Sephora reorders a search while the Actor reads it: another product can then be skipped. The run tells you, in its log and its last message ("N products seen twice in <search>: the site reordered its results while reading, some may be missing"); run it again, or use narrower searches, to get the missing ones.
- Two runs sharing the same `stateKey` at the same time may both return the same new product.

**A run that reaches its timeout** stops itself about 45 seconds before it: no new page is asked, what it read is saved and, with `onlyNew`, remembered, and the run ends *Succeeded* with "Stopped before the run's timeout". Resurrect it to go on from there, or give the next run a longer timeout (Run options). A product with thousands of reviews takes a while: cap them with `maxReviewsPerProduct`.

**A run the platform stops without warning** (out of memory)

- Resurrect it: it goes on from where it stood at most a minute before the stop. What it had read since is read again, and the products already saved are skipped: none is delivered or charged twice, and `maxItems` still counts them.
- With `onlyNew`, the memory is saved after each write (at most once every 2 seconds): resurrect the stopped run and the products it had saved meanwhile join the memory; leave it stopped for good, and the next run may return the products of its last 2 seconds once more.

#### Something doesn't work?

The last line of the log counts the products saved, filtered out and no longer on Sephora (removed while the run was reading them), and the requests that failed after every retry. Those requests and the removed products are listed, with the reason, in the `FAILED_REQUESTS` record of the run's key-value store. A run that saved nothing and had failed requests fails, and its last message gives the cause (a filter the site does not know says so, instead of "run it again").

If Sephora changes its pages, you are told instead of paying for blank rows. A results page that counts products but gives none the Actor can read is an error (listed in `FAILED_REQUESTS`), never a quiet "No products found". If the first 20 products read all lack their name, or all lack their price, the run saves nothing more, stops and fails, and its last message names the missing field: at most those first products are charged. A product that a filter drops counts among those 20. A run of fewer products (a scheduled `onlyNew` run that finds a few new ones, a small **Max products**) is checked at its end, from 5 products: its products are already saved, and the run fails with the missing field instead of ending green.

### 🛟 Support

Open an issue in the **Issues** tab with a link to your run: the run log and the `FAILED_REQUESTS` record of the key-value store show exactly which URLs failed and why.

# Actor input Schema

## `startUrls` (type: `array`):

sephora.com URLs: search (`/search?keyword=…`), category (`/shop/…`), brand (`/brand/…`) or product pages (`/product/…-P123456`). Filters set on the site are kept, pagination is automatic. A price bound or a sort already in a URL wins over the form's (a warning says so); a price bound the URL lacks is completed by the price fields below. Canada URLs (`/ca/en/…`) switch the country. When this list is not empty, the keyword / brand / category / product ID fields below are ignored; filters, caps, details, reviews and monitoring still apply. Max 1 000 URLs.

## `query` (type: `string`):

Free-text search, exactly as typed in the Sephora search box (e.g. `mascara`, `vitamin c serum`).

## `searchQueries` (type: `array`):

Several keywords in one run: one search per keyword. Added to **Search keyword**; products found by several searches are saved once.

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

Whole brand catalogues: brand name or the slug of its Sephora page (`lancome` for sephora.com/brand/lancome, `Rare Beauty`, `the-ordinary`). One search per brand.

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

Whole categories: the slug of a Sephora category page (`mascara` for sephora.com/shop/mascara, `face-serum`, `perfume`) or a category id (`cat140006` = Makeup). One search per category. A slug the site does not have (check that sephora.com/shop/<slug> opens) is skipped with a warning.

## `productIds` (type: `array`):

Sephora product ids (`P467208`, shown at the end of every product URL). Each one is read from its product page (full details).

## `country` (type: `string`):

Sephora store to read: United States (prices in USD) or Canada (prices in CAD, Canadian assortment).

## `language` (type: `string`):

Language of names, descriptions and reviews (French: only the reviews written in French; questions are always all of them, in the language they were written in). French is only available with **Country** = Canada.

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

Maximum number of products to save for the whole run (after deduplication and filters). 0 = no limit.

## `maxItemsPerQuery` (type: `integer`):

Cap for EACH search (keyword, brand, category or URL), so that the first search cannot use up the whole **Max products** budget. 0 = no per-search cap.

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

Order of the results, as on the site. Combined with the caps above it decides WHICH products you get (e.g. the 50 best sellers).

## `extractDetails` (type: `boolean`):

Open each product page (1 extra request per product) for every shade / size with its own price, sale price and stock status, full description, ingredients, how to use, highlights (clean, vegan…), all photos and videos, loves count, category path and Sephora's AI review summary. Charged per product (see Pricing). Off = search-card fields only.

## `includeReviewStats` (type: `boolean`):

Add the number of reviews per star (1-5), the number of reviewers who recommend the product and the product barcodes (EAN / GTIN) — 1 light extra request per product. Charged per product (see Pricing). Included when **Include reviews** is on.

## `includeReviews` (type: `boolean`):

Add the customer reviews of each product: rating, title, text, date, recommended or not, helpful votes, photos, brand reply, verified / incentivized / staff flags and the reviewer profile (skin type, skin tone, eye and hair color, skin concerns, age range). Charged per review (see Pricing).

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

Reviews to add per product (100 per request). 0 = all of them, up to about 9,000 reviews per product with 1 GB of memory or more, about 4,000 at the default 512 MB: a dataset row holds at most 7 MB, and the Actor fits it to the run's memory.

## `reviewsSort` (type: `string`):

Which reviews come first (and therefore which ones you get under the cap above).

## `reviewsPostedAfter` (type: `string`):

Only reviews submitted on or after this date: `2026-09-01`, or a period before now such as `7 days`, `1 month`. With **Newest first** the Actor stops reading a product's reviews at the first older one.

## `reviewsMinRating` (type: `integer`):

Only reviews with at least this many stars (1-5). Empty = all.

## `reviewsMaxRating` (type: `integer`):

Only reviews with at most this many stars (1-5), e.g. 2 for the negative reviews. Empty = all.

## `includeQuestions` (type: `boolean`):

Add the customer questions of each product with their answers (brand answers flagged), all languages, each as written. Charged per question (see Pricing).

## `maxQuestionsPerProduct` (type: `integer`):

Questions to add per product, newest first. 0 = all of them.

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

Only products with a price at or above this amount (in the store currency). Applied by Sephora. A pasted search URL that already has a minimum price keeps its own.

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

Only products with a price at or below this amount (in the store currency). Applied by Sephora. A pasted search URL that already has a maximum price keeps its own.

## `minRating` (type: `string`):

Only products rated at least this many stars. Applied by Sephora.

## `filterBrands` (type: `array`):

Keep only these brands inside a keyword or category search (brand name as shown on Sephora, e.g. `Lancôme`, `Rare Beauty`). Applied by Sephora.

## `newArrivalsOnly` (type: `boolean`):

Only the products Sephora marks as new. Applied by Sephora.

## `refinements` (type: `array`):

Any other filter of the Sephora sidebar, as the site writes it in the URL after `ref=`: `filters[ingredientPreferences]=crueltyFree`, `filters[SkinType]=oily`, `filters[benefits]=longWearing`, `filters[Pickup]=`, `filters[SameDay]=`. A filter the site does not know stops the run with a clear message instead of silently returning nothing.

## `saleOnly` (type: `boolean`):

Only products currently on sale.

## `exclusiveOnly` (type: `boolean`):

Only products sold exclusively at Sephora.

## `bestsellersOnly` (type: `boolean`):

Only products flagged as best sellers. Sephora shows that badge on search results only: a product given by ID or URL has none and is kept.

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

Drop the products whose every shade / size is out of stock. Needs **Extract product details** (stock is only on the product page); ignored otherwise.

## `excludeKeywords` (type: `array`):

Drop the products whose name or brand contains one of these words (case and accents ignored).

## `onlyNew` (type: `boolean`):

Skip the products that a previous run (same **Memory key**) already delivered: they are not saved and not charged, and their page is not even opened. First run = everything is new.

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

Name of the memory used by **Only new products**. Give each schedule / task its own key (e.g. `new-skincare`) so that they do not share their memory. Letters, digits, `-` and `_`.

## `resetState` (type: `boolean`):

Forget everything remembered under this **Memory key** before the run: this run returns (and charges) every product again. Untick it afterwards.

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

Apify Proxy or your own proxies. Keep the default: it is included in the price. The residential Apify proxy is not available in this Actor.

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

Maximum number of requests processed in parallel.

## `maxRequestsPerMinute` (type: `integer`):

Most requests to the site in any 60 seconds, all of them counted: search pages, product pages, reviews and questions. A budget, not an even pace: up to this many can leave at once when the minute starts (spread them with the minimum delay below). Lower it if the site answers HTTP 429 / 403 in the log.

## `minRequestIntervalMs` (type: `integer`):

Smallest gap between two requests, in milliseconds. Unlike the per-minute rate, this spreads the requests evenly instead of letting them go out in a burst. 0 = no gap.

## `maxRequestRetries` (type: `integer`):

Retries per request before it is marked as failed. Behind a proxy, a request the site turns away is also retried on a new proxy session up to 10 times without using up these retries.

## `debugLog` (type: `boolean`):

Include debug messages in the run log.

## Actor input object example

```json
{
  "startUrls": [],
  "query": "mascara",
  "searchQueries": [],
  "brands": [],
  "categories": [],
  "productIds": [],
  "country": "US",
  "language": "en",
  "maxItems": 100,
  "maxItemsPerQuery": 0,
  "sortBy": "relevance",
  "extractDetails": false,
  "includeReviewStats": false,
  "includeReviews": false,
  "maxReviewsPerProduct": 20,
  "reviewsSort": "newest",
  "includeQuestions": false,
  "maxQuestionsPerProduct": 10,
  "minRating": "",
  "filterBrands": [],
  "newArrivalsOnly": false,
  "refinements": [],
  "saleOnly": false,
  "exclusiveOnly": false,
  "bestsellersOnly": false,
  "inStockOnly": false,
  "excludeKeywords": [],
  "onlyNew": false,
  "stateKey": "default",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 8,
  "maxRequestsPerMinute": 120,
  "minRequestIntervalMs": 0,
  "maxRequestRetries": 5,
  "debugLog": false
}
```

# Actor output Schema

## `results` (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 = {
    "query": "mascara",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("nice_dev/sephora-products-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 = {
    "query": "mascara",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("nice_dev/sephora-products-scraper").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 '{
  "query": "mascara",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call nice_dev/sephora-products-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/sephora-products-scraper"
        }
    }
}
```

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/SuJJD3BOLYQ2oWbCH/builds/eZQ4SSIN9XejdgUje/openapi.json
