# Ulta Scraper (`autofacts/ulta-scraper`) Actor

Ulta web scraper to crawl product information including price and sale price, color, and images.

- **URL**: https://apify.com/autofacts/ulta-scraper.md
- **Developed by:** [Richard Feng](https://apify.com/autofacts) (community)
- **Categories:** Developer tools, E-commerce, Automation
- **Stats:** 42 total users, 3 monthly users, 100.0% runs succeeded, 3 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.50 / 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Ulta Scraper

**Ulta Scraper** provide a way to crawl product details of site Ulta.

### Features

1. Support fetch products of category;
2. Support fetch product detail with prices,descriptions,images and sku info;
3. Group skus in same product.
4. Per-shade **undertone/finish** description, product **size**, and **highlights** badges.

> The price values are multiplied by 100 to avoiding floating point calculations.

### How it works

Instead of scraping the WAF-protected `www.ulta.com` website HTML, this actor talks
directly to the **Ulta mobile app's GraphQL API** (`api.ulta.com/dxl/graphql`). That
endpoint runs the same "DXL" content backend the website uses, but lives on separate
infrastructure that answers anonymous read requests with a static app API key — no
login, no browser, and no bot challenge. The request contract (host, headers, API key,
GraphQL operation) was recovered from the official Android app.

For each start URL the actor:

1. Resolves the page path to a DXL `NonCachedPage` query.
2. For listing pages, reads the inlined `ProductListingResults` items and follows the
   `loadMoreAction` to page through all results.
3. For product pages, reads the `ProductPricing`/`ProductVariant`/`MediaGallery`/… modules
   and re-renders the page per SKU to collect each variant's price and images.

> **Cost note — all SKUs are merged into one product.**
> Ulta lists every shade/size as a separate SKU. This scraper groups all of them under a
> single product record (one dataset item with a `variants[]` array) rather than emitting
> one item per SKU. The trade-off is request cost: a product's *default* SKU comes free with
> the product page, but every **additional** SKU needs its own API call to read that variant's
> own price and images. So a product with N variants costs roughly **N requests** — and
> products with many shades (foundations, concealers) dominate the run time and cost.
>
> Use **`maxVariantFetches`** to cap the per-product variant calls. Variants beyond the cap
> are still included, using the product-level price and their single listing image (no extra
> call), so you trade some per-variant price/image accuracy for a much cheaper, faster run.

### Input Parameters

The input of this scraper shoule be JSON formated. Fields are:

| Field               | Type    | Description                                                                                          |
|:--------------------|:--------|:-----------------------------------------------------------------------------------------------------|
| startUrls           | Array   | Start URLs of Ulta site to start the scraper. Category page, product page urls are all supported.    |
| proxy               | Object  | Proxies for the api.ulta.com app API. `DATACENTER` (or no proxy) is recommended — `RESIDENTIAL` often adds upstream 502/504/ECONNRESET against this mobile API. Use `RESIDENTIAL` only if you see blocks. |
| maxConcurrency      | Object  | Actor running max concurrency, which helps you to not getting blocked.                               |
| maxRequestsPerCrawl | Integer | Maximum number of requests that can be made by this crawler, 0 to ignore.                            |
| maxVariantFetches   | Integer | Cap of per-product variant detail calls (each non-default shade/size costs one extra API call). Empty = fetch all. |

### Supported Pages

Supported of fetch data from below pages:

| Page                | Example                                                                                            |
|:--------------------|:---------------------------------------------------------------------------------------------------|
| Product Detail Page | https://www.ulta.com/p/studio-fix-powder-plus-foundation-makeup-xlsImpprod15921242?sku=2510752     |
| Category Page       | https://www.ulta.com/shop/makeup/face                                                              |
| Brand Page          | https://www.ulta.com/brand/ulta-beauty-collection                                                  |
| Sale Page           | https://www.ulta.com/promotion/sale                                                                |

### Data storage

Ulta scraper stores the product data to default data set in JSON format.

```json
{
	"source": {
		"id": "pimprod2048637",
		"crawlUrl": "https://www.ulta.com/p/radiance-conscious-beauty-kit-pimprod2048637?sku=2630776",
		"canonicalUrl": "https://www.ulta.com/p/radiance-conscious-beauty-kit-pimprod2048637?sku=2630776",
		"retailer": "ulta",
		"currency": "USD"
	},
	"brand": "Beauty Finds by ULTA Beauty",
	"title": "Radiance Conscious Beauty Kit",
	"description": {
		"short_desc": "Unwrap Radiance with Our Beauty Kit! Celebrate the season with the ultimate gift of clean beauty. Our beauty kit features luxurious, eco-friendly products designed to nourish and glow.",
		"full_desc": "#### Includes\n\n- Bubble, Skincare Day Dream Tone + Texture Serum Vitamin C + Niacinamide (0.17 oz)\n- Dermalogica, Special Cleansing Gel (0.5 oz)\n- House Of Lashes, Boudoir Lite Full False Lashes (1 pair)\n- Lolavie, Glossing Detangler (0.85 oz)\n- Nemat, Vanilla Musk Roll-On Fragrance Oil (0.17 oz)\n- ‘Ôrǝbella, NIGHTCAP Parfum (0.05 oz)\n- Peace Out, Acne Dots (4 dot patches)\n- Pür Beauty, Fully Charged Mascara Powered By Magnetic Technology Mini (0.14 oz)\n- St. Tropez, St. Tropez Self Tan Purity Bronzing Water Face Mist (0.47 oz)\n- Sunday Riley, Good Genes All-In-One Lactic Acid Treatment (0.17 oz)\n- Thayers, Thayers PH Cleanser (3.0 oz)\n- Viviscal, Thickening Conditioner (1.7 oz)\n- Viviscal, Thickening Shampoo (1.7 oz)\n\n"
	},
	"categories": [
		"Gifts",
		"By Price",
		"$50 and Under"
	],
	"options": [],
	"variants": [
		{
			"id": "2630776",
			"sku": "2630776",
			"options": [],
			"price": {
				"list": 4000,
				"listFormatted": "$40.00",
				"sale": 2400,
				"stockStatus": "InStock"
			},
			"medias": [
				{
					"id": "2630776",
					"type": "Image",
					"url": "https://media.ulta.com/i/ulta/2630776",
					"alt": "Beauty Finds by ULTA Beauty Radiance Conscious Beauty Kit #1",
					"width": 2000,
					"height": 2000
				},
				{
					"id": "2630776_alt01",
					"type": "Image",
					"url": "https://media.ulta.com/i/ulta/2630776_alt01",
					"alt": "Beauty Finds by ULTA Beauty Radiance Conscious Beauty Kit #2",
					"width": 2000,
					"height": 2000
				}
			]
		}
	],
	"stats": {
		"rating": 4.4,
		"reviewCount": 7
	},
	"price": {
		"sale": 2400,
		"list": 4000,
		"listFormatted": "$40.00",
		"stockStatus": "InStock"
	}
}
```

#### Product attributes (undertone / finish, size, highlights)

For makeup with shades, each colour option value carries a `description` — the
"undertone / finish" line Ulta shows under the swatch. The product also carries a
`size` (volume/dimension) and a `highlights` badge row (Clean Ingredients, Cruelty
Free, Vegan, …). `description` is omitted for shades Ulta doesn't describe (e.g. some
eyeliners), and `highlights`/`size` are omitted when the product has none.

```json
{
	"title": "Double Wear Stay-in-Place Longwear Matte Foundation",
	"size": "1.0 oz",
	"highlights": [
		{ "label": "Sustainable Packaging", "icon": "SustainablePackaging", "imageUrl": "https://media.ultainc.com/i/ulta/Sustainable-Packagingicon", "description": "..." }
	],
	"options": [
		{
			"type": "Colour",
			"values": [
				{
					"id": "1C1 Cool Bone",
					"name": "1C1 Cool Bone",
					"description": "light with cool rosy-peach undertones",
					"swatchIcon": { "url": "https://media.ultainc.com/i/ulta/2651973_sw", "alt": "1C1 Cool Bone …" },
					"smooshIcon": { "url": "https://media.ultainc.com/i/ulta/2651973_sm", "alt": "1C1 Cool Bone …" }
				}
			]
		}
	]
}
```

# Actor input Schema

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

Start URLs of Ulta to start the actor. Category page, product page urls are all supported.

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

Maximum number of requests that can be made by this crawler, 0 to ignore.

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

Proxies for the api.ulta.com app API. This is a mobile API (not the WAF-protected website), so a DATACENTER proxy or no proxy is usually faster and more reliable than RESIDENTIAL — residential IPs frequently return upstream 502/504/ECONNRESET against this host. Try datacenter first; switch to RESIDENTIAL only if you start seeing blocks.

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

Actor running max concurrency, which helps you to not getting blocked.

## `maxVariantFetches` (type: `integer`):

Each non-default product variant (shade/size) requires one extra API call to read its own price and images. Cap how many of those per product to bound run time on products with many variants. Leave empty to fetch all variants.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.ulta.com/shop/makeup/face"
    }
  ],
  "maxRequestsPerCrawl": 3,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  },
  "maxConcurrency": 5
}
```

# Actor output Schema

## `products` (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 = {
    "startUrls": [
        {
            "url": "https://www.ulta.com/shop/makeup/face"
        }
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyCountry": "US"
    },
    "maxConcurrency": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("autofacts/ulta-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 = {
    "startUrls": [{ "url": "https://www.ulta.com/shop/makeup/face" }],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyCountry": "US",
    },
    "maxConcurrency": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("autofacts/ulta-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 '{
  "startUrls": [
    {
      "url": "https://www.ulta.com/shop/makeup/face"
    }
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  },
  "maxConcurrency": 5
}' |
apify call autofacts/ulta-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,autofacts/ulta-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/ZEuUiKH1sIvP0TjBv/builds/mwSGDkzZLnFtXnnUd/openapi.json
