# Sephora Scraper - Products, SKUs, Reviews & 21+ Stores (`abotapi/sephora-product-scraper`) Actor

From $1/1K. Scrape Sephora products across 21+ storefronts (US, AU, NZ, SG, MY, ID, TH, PH, MX, DE, RO, SE, GR, DK) with a unified schema. Extract brand, title, ingredients, claims, full SKU and shade variants, ratings histogram, top reviews, Q\&A, and rich media.

- **URL**: https://apify.com/abotapi/sephora-product-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 14 total users, 3 monthly users, 94.6% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.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.
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

## Sephora Scraper: Product Catalog, Variants, Prices & Reviews

Sephora Scraper turns 14 regional Sephora storefronts into one clean product data feed. Get brand, title, ingredients, claims, the full variant matrix with every shade, size and SKU, pricing and discount fields, ratings, top reviews and top questions and answers, all normalized into a single output shape. Search by keyword or category, paste a URL, or track the Sale collection, then export to JSON, CSV or Excel, or read the results through the API.

### Why This Scraper?

- **14 regional storefronts, one input form.** United States, Australia, New Zealand, Singapore, Malaysia, Indonesia, Thailand, Philippines, Mexico, Germany, Romania, Sweden, Greece and Denmark, all normalized to the same output shape.
- **Three ways to find products.** Search by keyword, browse by category slug, or paste any Sephora product, category, brand or search URL and let the scraper detect the storefront.
- **Full product detail on demand.** Switch on the product detail fetch for ingredients, how-to-use text, claims and, on the US and Southeast Asia storefronts, the complete variant matrix (every shade, size, SKU and UPC). Leave it off for a search-results-only run so you only pay for the fields you actually use.
- **Ratings and voice of the customer.** Pull the rating histogram, top reviews (with skin type and skin tone tags where available) and top questions and answers, each capped by you.
- **Built-in discount tracking.** Every record carries `isOnSpecial`, `wasPrice`, `savingsAmount` and `savingsPercent`, always present, never guessed. A non-discounted product gets an explicit `false`/`null` rather than a missing field.
- **Resilient long walks.** Forward-paginated result walks checkpoint themselves and resume automatically after a platform restart, and a delta-resume option skips everything a previous run already collected.
- **Built for monitoring.** Incremental mode classifies every product as NEW, UPDATED or UNCHANGED against the previous run's state, so a recurring schedule returns only what changed.

### Use Cases

- **Retail price and assortment monitoring:** track price, stock status and the Sale collection across storefronts and currencies.
- **Beauty affiliate and comparison sites:** pull brand, description, ingredients and claims to build product pages without manual entry.
- **Review and sentiment research:** collect top reviews with skin type and skin tone context, plus the ratings histogram, for a brand's own SKUs or its competitors.
- **Variant and shade planning:** build a full shade, size and SKU matrix per product for merchandising or QA.
- **Recurring monitoring:** schedule the actor and use Incremental mode to see only new, changed or discontinued products since the last run.

### Data You Get

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

| Field | Example |
| --- | --- |
| `market` | `"sea-nz"` |
| `source.id` / `source.canonicalUrl` | `500000001` / `"https://www.sephora.nz/products/sample-foundation"` |
| `source.currency` / `source.locale` | `"NZD"` / `"en-NZ"` |
| `brand` | `"Sample Brand"` |
| `title` / `subtitle` | `"Sample Foundation"` / `"30ml"` |
| `description` | `"<p>A self-priming foundation, primes, covers and sets...</p>"` |
| `ingredients` | `"Aqua, Cyclopentasiloxane, Glycerin, ..."` |
| `howToUse` | `"Shake well. Apply a small amount onto the back of your hand..."` |
| `claims` | `["Cruelty free", "Paraben free", "Vegan"]` |
| `currentSku` | `"0000000"` |
| `price.current` / `price.original` | `18.0` / `36.0` |
| `categories` | `["Makeup", "Face", "Foundation"]` |
| `options[].values[].value` | `"1 Fair Neutral"` |
| `variants[].sku` / `variants[].shade` | `"0000000"` / `"1 Fair Neutral"` |
| `variants[].price.current` / `variants[].price.original` | `18.0` / `36.0` |
| `variants[].price.stockStatus` | `"IN_STOCK"` |
| `medias[].url` | `"https://image.example.com/products/sample-zoom.jpg"` |
| `stats.rating` / `stats.reviewCount` | `4.8` / `971` |
| `ratingHistogram` | `[{"RatingValue": 5, "Count": 700}, ...]` (when reviews are on) |
| `topReviews[].rating` / `topReviews[].body` | `5` / `"Best foundation I have ever tried..."` (when reviews are on) |
| `topQuestions[].question` | `"Is this product fragrance free?"` (when Q\&A is on) |
| `isOnSpecial` / `wasPrice` / `savingsAmount` / `savingsPercent` | `true` / `36.0` / `18.0` / `50` |
| `soldOut` | `false` |
| `frontend` | `"sea"` (also `"us"` or `"scc"` on a detail-enriched record; on a search-results-only record, the storefront's own code, e.g. `"fr"`, `"me"`, `"mx"`, `"br"`) |
| `scrapedAt` | `"2026-01-01T00:00:00.000Z"` |

The table above shows a detail-enriched Southeast Asia record. The shape varies by storefront and by whether details were fetched. With Fetch product details off, a record only carries the search-results fields: `market`, `source`, `brand`, `title`, top-level `price`, a `priceText`, `thumbnail`, `rating`, `reviewCount`, `isNewArrival`, `soldOut`, `frontend` and `scrapedAt`; it has no `description`, `ingredients`, `howToUse`, `claims`, `options`, `variants` or `stats`. Detail-enriched records from the European storefronts (Germany, Romania, Sweden, Greece, Denmark, plus France, Italy, Spain, Portugal, Poland and Brazil in url mode) have no `variants`, `options`, `claims` or `subtitle`. `claims` and `subtitle` are only produced for the Southeast Asia storefronts.

When Incremental mode is on, every record also carries `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED`), `changedFields` (the list of top-level fields that changed on an `UPDATED` record), and `firstSeenAt`/`lastSeenAt` timestamps; these four fields are absent on every normal (non-incremental) run.

### How to Use

1. Pick a **mode**: `search` (choose a storefront, then fill in keywords or category slugs) or `url` (paste one or more Sephora links; the scraper detects the storefront from the host).
2. In search mode, narrow the results with the optional filters (brand, price range, minimum rating, new arrivals, exclusives). These filters also apply to a pasted category or search URL in url mode, but not to a single pasted product URL, which always returns that exact product.
3. Switch on product details, reviews and Q\&A for the fields you need, and set **Max products** (and optionally **Max pages**) to control the size and cost of the run.
4. Set the connection to a residential proxy pinned to the storefront's country, then click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

The storefront dropdown covers the 14 sites above. Paste a URL from a Sephora site the dropdown does not list, such as France, Italy, Spain, Portugal, Poland, Brazil or the Middle East (UAE, Saudi Arabia, Qatar, Kuwait), and URL mode still detects the storefront from the link. Top reviews and top Q\&A are populated on the Southeast Asia storefronts today. On the US storefront they are not verified in this release and usually come back empty; other storefronts leave those fields empty even with the switches on.

**Search by keyword:**

```json
{
  "mode": "search",
  "storefront": "us",
  "queries": ["foundation", "mascara"],
  "maxPages": 5,
  "maxProducts": 100,
  "fetchDetails": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

**Search by category, with filters, reviews and Q\&A:**

```json
{
  "mode": "search",
  "storefront": "nz",
  "categories": ["shop/skincare"],
  "ratingMin": 4,
  "maxPages": 3,
  "maxProducts": 60,
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerProduct": 10,
  "fetchQna": true,
  "maxQnaPerProduct": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "NZ"
  }
}
```

**Specials (Sale collection, Southeast Asia storefronts):**

```json
{
  "mode": "search",
  "storefront": "com.au",
  "specialsOnly": true,
  "maxPages": 3,
  "maxProducts": 60,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "AU"
  }
}
```

**URL mode supported URL shapes:**

| Shape | Example | Status |
| --- | --- | --- |
| US search | `https://www.sephora.com/search?keyword=foundation` | Verified (returns product cards); the prefilled examples use only this shape (keywords `foundation` and `mascara`) |
| US category | `https://www.sephora.com/shop/foundation-makeup` | Not verified, may return no products |
| US product detail | `https://www.sephora.com/product/rare-beauty-by-selena-gomez-soft-pinch-liquid-blush-P97989778` | Not verified in this release |
| US brand page | `https://www.sephora.com/brand/rare-beauty` | Not verified in this release |
| New Zealand search | `https://www.sephora.nz/search?keyword=foundation` | Not verified in this release |
| New Zealand product detail | `https://www.sephora.nz/products/rare-beauty-true-to-myself-natural-matte-longwear-foundation` | Not verified in this release |

Behaviour change: in URL mode, if you leave the URLs field untouched (or omit it), the run now scrapes the two verified US search examples above instead of only one New Zealand product. Clear the field and paste your own links to scrape something else. If you send `startUrls`, the run switches to URL mode and scrapes only those links (plus any `urls` you typed yourself), never the untouched example list. A category, search or brand URL that returns no products logs `Could not load results page` with the URL.

**URL mode (single product):**

```json
{
  "mode": "url",
  "urls": [
    "https://www.sephora.nz/products/rare-beauty-true-to-myself-natural-matte-longwear-foundation"
  ],
  "fetchDetails": true,
  "fetchReviews": true,
  "fetchQna": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "NZ"
  }
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/sephora-product-scraper").call(run_input={
    "mode": "search",
    "storefront": "us",
    "queries": ["foundation"],
    "maxProducts": 20,
})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["brand"], product["title"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/sephora-product-scraper').call({
  mode: 'search',
  storefront: 'us',
  queries: ['foundation'],
  maxProducts: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Specials, was-price and discount

Sephora's Southeast Asia storefronts (Australia, New Zealand, Singapore, Malaysia, Indonesia, Thailand, Philippines) have exactly one discount collection, the site's own "Sale" page. Set `specialsOnly: true` to scrape it. The Sale collection does not combine with `queries` or `categories`, so those inputs are ignored while it is on and the whole Sale collection is scraped instead. `specialsOnly` only affects those Southeast Asia storefronts; on any other storefront it is a no-op and your normal queries or categories run as usual.

Every product record, whether from a normal search, a URL, or a `specialsOnly` run, carries four additive, always-present discount fields: `isOnSpecial` (boolean), `wasPrice` (the strike-through original price, `null` when not discounted), `savingsAmount` and `savingsPercent`. A non-discounted product gets `isOnSpecial: false` and `wasPrice: null`, never a fabricated markdown. `soldOut` (boolean) reports stock status.

#### Output views

The dataset has seven views in the Console: **Overview** (one row per product), **Variants** (one row per shade, size or SKU), **Ratings & reviews**, **Product detail** (description, ingredients, claims, how to use), **Reviews** (one row per review), **Questions & answers** (one row per question) and **Changes (incremental)** (`changeType`, `changedFields` and timestamps).

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search, category or URL set. The first run returns everything as `NEW`. Later runs classify each product as `NEW`, `UPDATED` (with `changedFields` naming what changed) or `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on) against the state saved from the previous run. `REAPPEARED` and `EXPIRED` only enter the picture once you turn on `emitExpired`: a product is reported `EXPIRED` only after a run scans the entire tracked scope without a Max products cap and without Resume being used, and a product only comes back as `REAPPEARED` if it was previously marked `EXPIRED` this way. If you never turn `emitExpired` on, you will only ever see `NEW`, `UPDATED` and `UNCHANGED`. Compared: every output field except `scrapedAt`. Nothing else is left out of change detection unless you list it in `ignoreFieldsForChanges` (for example `rating`, `reviewCount`, `stats.reviewCount`, `topReviews`), so rating and review-count moves count as `UPDATED` by default; listed fields stay in the output. A pasted URL starting at page 2 or later never produces `EXPIRED`. `stateKey` names or shares the stored state; leave it empty and the actor derives one automatically from the storefront, search terms and filters. With Incremental mode off, output is exactly as before.

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

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape, and the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you haven't authorized a connector yet.
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` or `url`. |
| `storefront` | string | `us` | Which of the 14 storefronts to scrape in search mode. |
| `market` | string | none | Alias for the storefront in the style `us`, `sea-nz`, `eu-de`, `mena-ae`; an explicit market wins over the default Storefront (`us`); if a non-default Storefront is also set the Storefront wins and a warning is logged. Unverified markets (uk, in, hk, tw, bn, cz, bh, om) are not supported: a warning is logged and Storefront is used. |
| `locale` | string | none | Alias accepted for compatibility (e.g. `en-AU`). The storefront decides the locale; a mismatch logs a warning. |
| `categoryIds` | array | none | Alias merged into Categories. |
| `newProductsPageLimit` | integer | none | Alias: with Only new arrivals on, caps the result pages walked (same as Max pages). |
| `maxRequestsPerCrawl` | integer | none | Alias: caps the result pages walked (same as Max pages). |
| `queries` | array | none (prefill: `["foundation"]`) | Keywords to search; one search per query. |
| `categories` | array | none (prefill: empty) | Category slugs from the storefront's navigation. |
| `sortBy` | string | `relevance` | `relevance`, `popular`, `rating`, `price-asc`, `price-desc`, or `newest`. |
| `specialsOnly` | boolean | `false` | Scrape the Sale collection instead of queries/categories (Southeast Asia storefronts only; a no-op elsewhere). |
| `urls` | array | one example per URL shape (see table below); `startUrls` is accepted as an alias | One or more Sephora URLs to crawl in url mode. |
| `brands` | array | none (prefill: empty) | Brand slugs to filter results. Applies in both modes, except a single pasted product URL. |
| `priceMin` | integer | none | Minimum product price in the storefront's currency. |
| `priceMax` | integer | none | Maximum product price in the storefront's currency. |
| `ratingMin` | integer | `0` | Minimum average rating (`0`, `3`, `4`, or `5`). |
| `newOnly` | boolean | `false` | Only products marked as new arrivals. |
| `onlyNewProducts` | boolean | `false` | Alias of `newOnly`; either one switches the filter on. |
| `exclusiveOnly` | boolean | `false` | Only products marked "Only at Sephora". |
| `fetchDetails` | boolean | `false` | Fetch the product detail page for ingredients, how-to-use, the full variant list and claims. Also required, in search mode, for reviews and Q\&A to be fetched even when those are turned on. |
| `fetchReviews` | boolean | `false` | Fetch the top reviews per product. Populated today on the Southeast Asia storefronts (US: not verified, usually empty). In search mode, also requires Fetch product details to be on. |
| `maxReviewsPerProduct` | integer | `10` | Cap reviews per product (1 to 100; the maximum is a hard cap, higher values are rejected). Ignored unless Fetch top reviews is on. |
| `fetchQna` | boolean | `false` | Fetch top questions and answers per product. Populated today on the Southeast Asia storefronts (US: not verified, usually empty). In search mode, also requires Fetch product details to be on. |
| `maxQnaPerProduct` | integer | `5` | Cap Q\&A entries per product (1 to 50). Ignored unless Fetch top Q\&A is on. |
| `maxPages` | integer | none (unlimited) | Optional bound on result pages walked per query, category or URL. Leave empty to walk every result page. |
| `maxProducts` | integer | `20` | Total cap across all queries/URLs (default 20, hard maximum 100000; `0` means the maximum). |
| `proxyConfiguration` | object | Residential (US) | Connection settings. A residential connection is what the US and European storefronts need to load reliably. |
| `resumeFromCheckpoint` | boolean | `true` | Resume a same-run migration or restart from the internal checkpoint. |
| `resumeFromRunId` | string | none | Paste a previous run or dataset ID to skip products it already collected. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring of the same search, category or URL set. |
| `ignoreFieldsForChanges` | array | none (nothing is ignored by default) | Incremental mode only: extra field names (dotted paths for nested keys, e.g. `stats.lovesCount`) left out of change detection. They stay in the output. |
| `stateKey` | string | none | Name or share an incremental-mode monitoring state. Leave empty to derive one automatically. |
| `emitUnchanged` | boolean | `false` | Also return (and bill for) products unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Also return (and bill for) products no longer found, marked `EXPIRED`, once a run fully scans the tracked scope. |
| `mcpConnectors` | array | none | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

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

```json
{
  "market": "sea-nz",
  "source": {
    "id": 500000001,
    "crawlUrl": "https://www.sephora.nz/products/sample-foundation",
    "canonicalUrl": "https://www.sephora.nz/products/sample-foundation",
    "retailer": "SEPHORA",
    "currency": "NZD",
    "locale": "en-NZ"
  },
  "brand": "Sample Brand",
  "title": "Sample Foundation",
  "subtitle": "30ml",
  "description": "<p>A self-priming foundation, primes, covers and sets...</p>",
  "shortDescription": "<p>3-in-1 foundation primes, covers and sets...</p>",
  "ingredients": "Aqua, Cyclopentasiloxane, Glycerin, ...",
  "howToUse": "Shake well. Apply a small amount onto the back of your hand and use a brush or fingertips to blend onto skin.",
  "claims": ["Cruelty free", "Paraben free", "Vegan"],
  "currentSku": "0000000",
  "price": {
    "current": 18.0,
    "original": 36.0,
    "currency": "NZD",
    "stockStatus": "IN_STOCK",
    "available": true,
    "inventory": 13
  },
  "categories": ["Makeup", "Face", "Foundation"],
  "options": [
    {
      "name": "shade",
      "id": "0",
      "values": [{ "value": "1 Fair Neutral", "label": "1 Fair Neutral", "id": "0" }]
    }
  ],
  "variants": [
    {
      "id": "0",
      "sku": "0000000",
      "gtin": "000000000000",
      "name": "1 Fair Neutral",
      "shade": "1 Fair Neutral",
      "size": "30ml",
      "price": {
        "current": 18.0,
        "original": 36.0,
        "currency": "NZD",
        "stockStatus": "IN_STOCK",
        "available": true,
        "inventory": 13
      },
      "highlights": ["NEW", "Only at Sephora"],
      "swatchUrl": "https://image.example.com/swatches/icon_swatch_0000.png",
      "imageUrl": "https://image.example.com/products/sample.jpg"
    }
  ],
  "medias": [
    { "url": "https://image.example.com/products/sample-zoom.jpg", "type": "image", "role": "hero" }
  ],
  "stats": {
    "reviewCount": 971,
    "rating": 4.8,
    "recommendedCount": 800,
    "totalPhotoCount": 100
  },
  "ratingHistogram": [
    { "RatingValue": 5, "Count": 700 },
    { "RatingValue": 4, "Count": 200 },
    { "RatingValue": 3, "Count": 50 },
    { "RatingValue": 2, "Count": 15 },
    { "RatingValue": 1, "Count": 6 }
  ],
  "topReviews": [
    {
      "id": "0",
      "rating": 5,
      "title": "Best foundation I have ever tried",
      "body": "Sample review text appears here...",
      "publishedAt": "2026-01-01T00:00:00.000Z",
      "skinType": "combination",
      "skinTone": "fair"
    }
  ],
  "highlights": ["NEW", "Only at Sephora"],
  "isOnSpecial": true,
  "wasPrice": 36.0,
  "savingsAmount": 18.0,
  "savingsPercent": 50,
  "soldOut": false,
  "frontend": "sea",
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

### Plan Requirement

**A paid Apify plan with residential proxy access is recommended, and a residential connection is not a guarantee.** Sephora's storefronts sit behind strict bot protection. In our measurements, plain requests through datacenter connections were refused (access denied pages on category, product and brand links) or answered with an empty placeholder page instead of products, and the New Zealand storefront refused even residential New Zealand connections to a plain request. We could not confirm that residential alone is sufficient for every storefront, so treat the United States and New Zealand storefronts as best effort and check a small run first.

What the actor does about it:

- It starts on the connection you select (the default is residential, pinned to the United States) and, when a request is refused, retries on a different connection and, if needed, on a backup connection.
- If the selected connection is unavailable to your account (for example on a free plan, which has no residential proxies), the log shows: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." When pages then load, the log adds "Served over the backup connection (the default connection was unavailable)."
- If no page could be read at all, the run FAILS with a message instead of returning an empty dataset: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required", or "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Sephora pages", or "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."
- You can supply your own proxy URLs in the proxy field instead.

Pin the connection's country to the storefront's home market, for example NZ for sephora.nz or DE for sephora.de. See the **Pricing** tab for current rates.

### FAQ

#### How much does it cost?

You pay per record returned (dataset item event), plus one detail-enrichment event per returned product when Fetch product details is on. When Fetch top reviews is also on, each returned product adds one more detail-enrichment event for every 10 reviews delivered (rounded up, so the default of 10 reviews adds 1 event and 20 reviews add 2); it is charged only for products actually returned and is capped by Max reviews per product (at most 100 reviews, i.e. 10 events). The **Pricing** tab on the actor page shows the exact current rates, and paid plans get lower rates. Use **Max products** to cap the cost of any run.

#### Is it legal to scrape Sephora?

This actor collects only publicly available product data. You are responsible for how you use it: follow Sephora's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product descriptions, images and review text may be subject to Sephora's or its brands' own rights.

#### Can I get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated products by default, and unchanged ones are suppressed and not billed. A product that disappears is only reported (as `EXPIRED`) once you turn on **Emit expired products** and a run scans the full tracked scope without being capped by Max products or run with Resume. A product that comes back after being marked `EXPIRED` is then reported as `REAPPEARED` on the run where it reappears. Without Emit expired products turned on at some point, you will only ever see `NEW`, `UPDATED` and `UNCHANGED`.

#### Why did my run return no reviews or Q\&A even though I turned them on?

Fetch top reviews and Fetch top Q\&A currently return data on the Southeast Asia storefronts (Australia, New Zealand, Singapore, Malaysia, Indonesia, Thailand, Philippines). The US storefront's review feed is not verified in this release and usually returns an empty list. Other storefronts return an empty list for these fields even with the switches on, since their review platform is not wired up yet. In search mode, both switches also require Fetch product details to be on; otherwise they are silently ignored.

#### Why did my run fail instead of returning an empty dataset?

An unresolvable input, such as an empty search with no queries or categories, leaves the run SUCCEEDED with zero rows and a status message explaining why, so a first-time typo never gets billed for nothing. The run fails outright only for a genuine setup problem: a Resume from run ID that cannot be resolved to a run or dataset your account can read, or Incremental mode combined with a Resume from run ID when that search already has saved incremental state. If the site refuses every request, run it again in a few minutes.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push a summary of each record into Notion, Linear or Airtable.

### 🔗 Want more e-commerce data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/lazada-scraper"><b>Lazada Scraper</b></a><br>Scrape Lazada products and reviews across SEA markets, including Malaysia, Singapore...</td><td>🛒 <a href="https://apify.com/abotapi/muji-scraper"><b>MUJI Scraper</b></a><br>Scrape MUJI products from the United States, Canada and Australia storefronts. Search by...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/shein-product-scraper"><b>SHEIN Scraper</b></a><br>Scrape SHEIN product listings by keyword or from any category, sale or search link...</td><td>🛒 <a href="https://apify.com/abotapi/officeworks-scraper"><b>Officeworks Scraper</b></a><br>Scrape Officeworks products by keyword, category, or URL. Extract names, brands, prices...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/douglas-de"><b>Douglas Germany Scraper</b></a><br>Scrape Douglas.de beauty and fragrance products by keyword, category, brand, or URL...</td><td>🛒 <a href="https://apify.com/abotapi/thewarehouse-co-nz-scraper"><b>The Warehouse NZ Scraper</b></a><br>Scrape The Warehouse New Zealand search results, category pages, and direct product URLs...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/sephora-product-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

Search mode = pick storefront + queries. URL mode = paste Sephora product, category, brand, or search URLs. Result filters (price, rating, brand, new arrivals, exclusives) work in both modes.

## `storefront` (type: `string`):

Which Sephora regional site to scrape.

## `market` (type: `string`):

Alias for the storefront in the style of other Sephora scrapers: us, eu-de, eu-ro, eu-gr, mena-ae, mena-sa, mena-qa, mena-kw, sea-au, sea-nz, sea-sg, sea-my, sea-id, sea-th, sea-ph. An explicit market wins over the default Storefront (us); if a non-default Storefront is also set, the Storefront wins and the run logs a warning. Markets without a verified storefront (uk, in, sea-hk, sea-tw, sea-bn, eu-cz, mena-bh, mena-om) are NOT supported: the run logs a warning and falls back to the Storefront value.

## `locale` (type: `string`):

Accepted for compatibility with other Sephora scrapers (e.g. en-AU). The storefront decides the locale; a mismatch logs a warning.

## `categoryIds` (type: `array`):

Alias that is merged into Categories (slugs or paths).

## `newProductsPageLimit` (type: `integer`):

With Only new arrivals on, caps the result pages walked per query (same as Max pages).

## `maxRequestsPerCrawl` (type: `integer`):

Caps the result pages walked per query (same as Max pages).

## `queries` (type: `array`):

Keywords to search the storefront (e.g. 'foundation', 'rare beauty', 'lipstick'). Each query runs as a separate search.

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

Category slugs from the navigation tree (e.g. 'shop/foundation-makeup', 'shop/skincare', 'shop/fragrance'). Optional, leave empty if you only want keyword search.

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

How to order the search results.

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

Scrape Sephora's 'Sale' collection instead of your queries/categories. Covers the SEA storefronts only (AU/NZ/SG/MY/ID/TH/PH); ignored elsewhere. The Sale collection does not combine with keywords or categories, so when this is on, 'Search queries' and 'Category slugs' are ignored and the full Sale collection is scraped instead.

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

One or more full Sephora URLs (product, category, brand, or search). The scraper auto-detects the storefront from the host. Left untouched, one example per URL shape is scraped. `startUrls` is accepted as an alias.

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

Brand slugs to filter results (e.g. 'rare-beauty', 'fenty-beauty', 'charlotte-tilbury'). Multiple allowed.

## `priceMin` (type: `integer`):

Minimum product price in the storefront's currency.

## `priceMax` (type: `integer`):

Maximum product price in the storefront's currency.

## `ratingMin` (type: `integer`):

Only return products with average rating at or above this value. Use 0 (no filter), 3, 4, or 5.

## `newOnly` (type: `boolean`):

Return only products marked as new arrivals on the storefront.

## `onlyNewProducts` (type: `boolean`):

Alias of 'New arrivals only'. Either one switches the filter on.

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

Return only products marked 'Only at Sephora'.

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

Fetch the product detail page for full content. Required for ingredients, howToUse, full variant list, claims, awards, ratings histogram. SERP-only mode is faster and cheaper.

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

Pull the top N reviews per product (sorted by helpfulness). Adds one extra fetch per product.

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

Cap the number of reviews per product (1-100). Ignored unless 'Fetch top reviews' is on.

## `fetchQna` (type: `boolean`):

Pull questions and answers per product (sorted by recency). Adds one extra fetch per product.

## `maxQnaPerProduct` (type: `integer`):

Cap the number of Q\&A entries per product (1-50). Ignored unless 'Fetch top Q\&A' is on.

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

Optional bound on result pages walked per query/category/URL. Leave empty to walk every result page; the walk stops when the storefront returns no next page, a repeat page is detected, or 'Max products total' is hit.

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

Total cap across all queries/URLs. Set to 0 for the hard maximum of 100000 (also bounded by maxPages and the storefront's own result count).

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

Residential proxy is required for the US storefront. AU/NZ/EU storefronts also work better with a residential proxy in the matching country. Default uses Apify's built-in proxy. Free plan: does not include residential, default proxy may return zero results on the US storefront.

## `resumeFromCheckpoint` (type: `boolean`):

Resume a previous run from the last saved checkpoint.

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

Optional: paste a previous run ID (or dataset ID) from this actor. Products already collected there are skipped, so this run only returns new ones. Leave empty for a normal run. For recurring daily/weekly monitoring of the same search or URLs, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring of the same search/category/URLs. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. UPDATED means a compared field changed: price, was price, specials, stock, variants, title, brand, categories, images, descriptions and every other product field. Review and rating counters (rating, reviewCount, stats ratings/loves/question counts, ratingHistogram, topReviews, topQuestions, sentiments) and scrapedAt are kept in the output but never trigger UPDATED; add more with "Ignore fields when detecting changes". Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately per search/category/URL and filter setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from run ID 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 storefront/search/category/URL and filter settings -- different setups 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/category/URLs -- not when Max products capped it or when Resume was used. This returns -- and bills -- extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to exclude from UPDATED change detection, in addition to the built-in handling (review and rating counters never trigger a false UPDATED). Use a top-level name such as "badges", or a dotted path for a nested key such as "stats.lovesCount". Ignored fields stay in the output; they are only left out of the comparison and of changedFields.

## `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",
  "storefront": "us",
  "queries": [
    "foundation"
  ],
  "categories": [],
  "sortBy": "relevance",
  "specialsOnly": false,
  "urls": [
    "https://www.sephora.com/search?keyword=foundation",
    "https://www.sephora.com/search?keyword=mascara"
  ],
  "brands": [],
  "ratingMin": 0,
  "newOnly": false,
  "onlyNewProducts": false,
  "exclusiveOnly": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 10,
  "fetchQna": false,
  "maxQnaPerProduct": 5,
  "maxProducts": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "resumeFromCheckpoint": true,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `products` (type: `string`):

No description

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

No description

## `detail` (type: `string`):

No description

## `variants` (type: `string`):

No description

## `ratings` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `questions` (type: `string`):

No description

## `changes` (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",
    "queries": [
        "foundation"
    ],
    "categories": [],
    "urls": [
        "https://www.sephora.com/search?keyword=foundation",
        "https://www.sephora.com/search?keyword=mascara"
    ],
    "brands": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/sephora-product-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",
    "queries": ["foundation"],
    "categories": [],
    "urls": [
        "https://www.sephora.com/search?keyword=foundation",
        "https://www.sephora.com/search?keyword=mascara",
    ],
    "brands": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/sephora-product-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 '{
  "mode": "search",
  "queries": [
    "foundation"
  ],
  "categories": [],
  "urls": [
    "https://www.sephora.com/search?keyword=foundation",
    "https://www.sephora.com/search?keyword=mascara"
  ],
  "brands": [],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/sephora-product-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/sephora-product-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/dC5NAnZ7E2ia5QEmA/builds/l7mO7sxuaSGqHGFCn/openapi.json
