# Dermstore Product & Sale Price Scraper (US) (`superslowsloth/dermstore-products`) Actor

Dermstore scraper: search terms or category, brand, ingredient and sale pages to one row per product with USD price, list price, discount percent, promo text and code, brand, size variant, stock and image. For US skincare price and promo watching.

- **URL**: https://apify.com/superslowsloth/dermstore-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 product scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

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

## Dermstore Product & Sale Price Scraper (US)

Search [Dermstore](https://www.dermstore.com) (a US skincare and beauty retailer)
by keyword, or read a category, brand, ingredient or sale page, and get one flat
row per product: the current price and the **list price in USD**, the dollars and
percent off, the promo text and its **promo code**, brand, size variant, whether
it can be bought, the product link and the photo. Run it on a schedule and diff
the dataset to get a **US skincare price and promo watch**: which serums, brands
or sale items dropped in price, which carry a gift-with-purchase, and which only
advertise a code.

No account, no API key, no login, no browser. One request returns 32 products.

### Input

| Field | Notes |
|---|---|
| `searchTerms` | One or more search terms, one per line (`vitamin c serum`, `cerave`, `retinol night cream`). Dermstore sends most terms it recognises to a category, ingredient or brand page, and that whole list is read; any other term is read as search results. A product found by two terms is returned once, under the first. |
| `startUrls` | Dermstore pages to read in full: a category (`/c/skin-care/treatments-serums/face-serum/`), a brand (`/c/brands/skinceuticals/`), an ingredient (`/c/ingredient/retinol/`) or the sale page (`/c/sale/`). A search URL (`/search/?q=...`) is read as that search term. Pages must list products; see the notes below for the few that do not. Give terms, URLs or both. |
| `maxItems` | Products to read per term or page (default 100, up to 10,000). 32 per page. |
| `onlyOnSale` | Return only products whose current price is below the list price. Products that are not reduced are read, not returned and not charged. |
| `sort` | `popularity` (default), `price-asc`, `price-desc`, `name`, `newest`: the five choices of the site's own sort menu. |
| `proxyConfiguration` | Apify datacenter proxy by default. |

Example input:

```json
{
  "searchTerms": ["vitamin c serum", "cerave"],
  "startUrls": [{ "url": "https://www.dermstore.com/c/sale/" }],
  "maxItems": 100,
  "sort": "popularity",
  "onlyOnSale": false
}
```

### Output

Prices are numbers in US dollars; `currency` is the currency the page states (USD
on every page measured).

| Field | Notes |
|---|---|
| `query`, `position` | The search term or page URL that first found the product, and its place in the site's list for that source and sort (1 is first). |
| `product_id`, `url` | Dermstore's product id (`11174243`; the site's "sku") and the product link. |
| `name`, `brand` | As the page shows them. `brand` is null if the page sends none. |
| `price`, `original_price` | `price` is the price now, the card's "Current price". `original_price` is the list price the card strikes through (Dermstore labels it "Recommended Retail Price"). They are equal when the product is not reduced. |
| `on_sale`, `discount_amount`, `discount_percent` | `on_sale` is true when `price` is below `original_price`. `discount_amount` is the dollars off, as the page sends it. `discount_percent` is worked out from the two prices (one decimal place). Both are **null when the product is not on sale**, never 0. |
| `promo`, `promo_code` | The promotion banner on the card: `Save 25%`, `Code SHIP, Unlock Free 2-Day Shipping at $150`, `Free Shipping + $20 Gift`, `Medik8 Gift`, `$65 Full-Size Gift`, `New Arrival`. `promo_code` is the code the text names (`SHIP`). Null when there is none. |
| `variant`, `variant_count` | The page's variant label and how many variants the product has. See the note below. |
| `in_stock`, `stock_label` | False with `stock_label` `COMING SOON` when the card shows a disabled button instead of "Quick buy". |
| `image_url` | The product photo, original size, on `static.thcdn.com`. |
| `list_name` | The list's own title in the site's tracking data (`Face Serums`, `CeraVe`). Null on search pages. |

Example row (real values from the sale page, 2026-10-16):

```json
{
  "query": "https://www.dermstore.com/c/sale/",
  "position": 13,
  "product_id": "11174243",
  "name": "Paula's Choice CLINICAL 1% Retinol Treatment (1 fl. oz.)",
  "brand": "Paula's Choice",
  "url": "https://www.dermstore.com/p/paula-s-choice-clinical-1-retinol-treatment-30ml/11174243/",
  "image_url": "https://static.thcdn.com/productimg/original/11174243-8085356454909766.jpg",
  "currency": "USD",
  "price": 48.75,
  "original_price": 65.0,
  "on_sale": true,
  "discount_amount": 16.25,
  "discount_percent": 25.0,
  "promo": "Save 25%",
  "promo_code": null,
  "variant": "30ml",
  "variant_count": 1,
  "in_stock": true,
  "stock_label": null,
  "list_name": "Dermstore Skin Care, Hair & Beauty Sale"
}
```

#### What the price fields mean

The page's own tracking data calls the list price `price` and the current price
`value`, which reads backwards. Which is which was checked against the product
cards on 2026-10-16 over 1,025 products (all 23 pages of the face serum category,
the sale list and a search): on every reduced product (193 of them) the
struck-through "Recommended Retail Price" on the card equals the list price, the
"Current price" equals the current price, and the dollars off equal the
difference, with no exception. Every "Save N%" banner measured sat on a reduced
product and matched its reduction.

**A promo code is not a reduction.** The most common banner, `Code SHIP, Unlock
Free 2-Day Shipping at $150`, is a free-shipping code; the card shows the full
price. `price` never includes a code's saving, and the offer is in `promo` and
`promo_code`. Filter on `promo_code` to see code offers, on `on_sale` for shelf
reductions, and on `promo` for gifts with purchase.

#### Honest nulls

A field the page did not send is `null`, never `0` or `""`.

- **A `COMING SOON` product can have no price.** On 2 of the 1,025 products
  measured the tracking data sent null for both prices although the card still
  printed one; the row is returned with null `price` and `original_price` rather
  than a figure the data did not carry.
- **No rating or review count.** The ratings are filled in by the browser after
  the page loads and are not in the HTML, so the rows do not carry them.
- **`variant` is not the size of the row.** It is the page's label for the
  product's default variant (the `Medik8 Crystal Retinal 6 Serum 30ml` row says
  `Retinal 10`). Read the size from `name` and `url`.
- **`in_stock` is about the card.** True means a working buy button, false a
  disabled one. The site does not say more (stock levels, delivery dates).

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.0021** per product row written (`product-scraped`), i.e. $2.10 per
  1,000 products. A row that is not written is not charged: rows filtered by
  `onlyOnSale`, and repeats of a product another term already returned, are free.

### How it fetches

It reads the product list that dermstore.com's own pages embed (an item map keyed
by product id) and the product cards next to it. Measured 2026-10-16: HTTP 200
HTML from a plain connection and from Apify datacenter addresses, no key or cookie
needed. A refusal (403, 429, 5xx), a 200 that is not a Dermstore page, or a
dropped connection is treated as the address being blocked, and the actor rotates
to a fresh one and retries.

Things worth knowing, all measured on 2026-10-16:

- **A search usually lands on a list page.** Dermstore redirects most terms it
  knows: `serum` to the Face Serums category (734 products), `retinol` and
  `vitamin c serum` to ingredient pages (211 and 359), `eye cream` to the eye
  cream category, `cerave` to the CeraVe brand page (50 products). Terms it does
  not redirect are answered in place (`retinol night cream`: 134 products, 5
  pages). The actor reads whatever list the site lands on, all its pages, and
  `query` stays the term you typed. A broad term can return a whole category;
  `maxItems` bounds it. The run log says where each term was sent.
- **Brand pages are product lists.** `/c/brands/cerave/`, `/c/brands/medik8/`,
  `/c/brands/skinceuticals/` and the other brand pages tried list their products
  directly and take the sort and page settings.
- **No results, and brands Dermstore does not carry.** A term the site cannot
  match (`zzzqqqxx`, and also `birthday gift`) goes to a "no search results"
  page. A brand it does not stock (`the ordinary`) goes to a "We don't stock this
  brand" page that lists thousands of unrelated products; the actor treats that
  as no results and returns nothing for the term rather than charge you for
  products you did not ask for.
- **The redirect drops `sort`.** The actor re-reads the first page from the
  landing list when you asked for a sort, so the order you chose is the order
  you get. The price sorts were in strict order on the pages checked.
- **A few pages are landing pages.** `/c/sunscreen/` is a page of carousels; it
  links its own list of everything (`/c/sunscreen/view-all/`, 498 products) and
  the actor reads that instead. The brand index `/c/brands/` links no such list
  and fails (at no charge) with a message rather than return nothing.
- **Depth.** The site lists 32 products per page and has no page-size setting.
  The largest category measured, `/c/skin-care/`, has 134 pages. A page past the
  last is an empty list and the actor stops there. At most 10,000 products per
  term or page.
- **Pages repeat nothing.** All 23 pages of the face serum category were read:
  734 products, none twice. A guard still ends a walk after three pages in a row
  that bring nothing new.
- **Not offered.** The site's facet filters (brand, price range, skin concern)
  are not wired into this actor; start from a category, ingredient or brand page
  that already narrows the list.

# Actor input Schema

## `searchTerms` (type: `array`):

What you would type into the Dermstore search box, one term per line (for example vitamin c serum, cerave or retinol night cream). Each term is read separately. Dermstore sends most terms it recognises to a category, ingredient or brand page, and that whole list is read; any other term is read as search results. A brand Dermstore does not carry, and a term it cannot match, return no products. A product that two terms both find is returned once, under the first.

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

Dermstore pages to read in full, for example https://www.dermstore.com/c/sale/, https://www.dermstore.com/c/brands/skinceuticals/ or https://www.dermstore.com/c/skin-care/treatments-serums/face-serum/. Use a page that lists products. A landing page that links its own list of everything (/c/sunscreen/ links /c/sunscreen/view-all/) is read through that list; one that links none, such as the brand index /c/brands/, is refused. A search URL (dermstore.com/search/?q=...) is read as that search term. Leave empty to use search terms only.

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

Read at most this many products for each search term or page. The site lists 32 per page, so 100 reads four pages. Up to 10,000 are accepted.

## `sort` (type: `string`):

The order Dermstore lists the products in, the same five choices as its own sort menu. 'Popularity' is its default. The price sorts follow the site's own price index.

## `onlyOnSale` (type: `boolean`):

Return only products whose current price is below the list price (the on_sale field). Products that are not reduced are read but not returned and not charged, and do not count toward Max products. A voucher code (for example the free-shipping code SHIP) is not a reduced price; read the promo and promo_code fields for those.

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

Use Apify's automatic datacenter proxy (the default). dermstore.com answered an Apify datacenter address with HTTP 200 when this actor was built; the residential group was not needed. A refused request is retried from a fresh address.

## Actor input object example

```json
{
  "searchTerms": [
    "vitamin c serum"
  ],
  "startUrls": [],
  "maxItems": 100,
  "sort": "popularity",
  "onlyOnSale": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "searchTerms": [
        "vitamin c serum"
    ],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/dermstore-products").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 = {
    "searchTerms": ["vitamin c serum"],
    "startUrls": [],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/dermstore-products").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 '{
  "searchTerms": [
    "vitamin c serum"
  ],
  "startUrls": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/dermstore-products --silent --output-dataset

```

## MCP server setup

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

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/7HtLDQow8iUhPIEmu/builds/X0U8XMAevX5YGImYy/openapi.json
