Ulta Beauty Scraper - Beauty Price & Sale Tracker (USA)
Pricing
from $0.28 / 1,000 product scrapeds
Ulta Beauty Scraper - Beauty Price & Sale Tracker (USA)
Ulta Beauty scraper: category and brand pages to one row per product with USD list and sale price (ranges split), discount, promo, rating, review count, badges, shade count, link and photo. For beauty price and sale watching by category or brand.
Pricing
from $0.28 / 1,000 product scrapeds
Rating
0.0
(0)
Developer
Superslow Sloth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Ulta Beauty scraper: beauty price and sale tracker by category or brand (USA)
This Ulta Beauty scraper reads Ulta Beauty (the US beauty retailer) category and brand pages and returns one flat row per product: the list price and the sale price in USD (a price range split into low and high), the discount in dollars, the promotion line, rating and review count, badges, how many shades the product has, the product link and the photo. Run it on a schedule and diff the dataset to get a beauty price and sale watch: which foundations or Maybelline products were marked down since last week, and by how much.
No account, no API key, no login, no browser. One request returns 64 products.
Input
| Field | Notes |
|---|---|
startUrls | One or more Ulta pages that list products: a category (https://www.ulta.com/shop/makeup/face/foundation, /shop/skin-care/moisturizers) or a brand (https://www.ulta.com/brand/maybelline, /brand/olaplex). A site path works as well as a full URL. Each page is read separately, in Ulta's own order. A product found by two pages is returned once, under the first. Anything after the path (?page=, filters, sort) is ignored. |
maxItems | Products to read per page (default 100, up to 10,000). 64 per page. |
proxyConfiguration | Apify datacenter proxy by default. |
A top-level category with no list of its own (/shop/makeup, /shop/skin-care)
shows only featured products, so the actor refuses it with a message instead of
returning those; pick a sub-category such as /shop/makeup/face/foundation. A
page Ulta does not have (/brand/fenty-beauty, a misspelt category) fails that
page at no charge and the run goes on with the others.
Example input:
{"startUrls": [{ "url": "https://www.ulta.com/shop/makeup/face/foundation" },{ "url": "https://www.ulta.com/brand/maybelline" }],"maxItems": 300}
Output
Prices are numbers in US dollars; currency is USD (every page measured).
| Field | Notes |
|---|---|
source, position | The Ulta page the product came from (/shop/makeup/face/foundation or /brand/maybelline), and its place in Ulta's order for that page (1 is first, counted over every card of the pages read). |
product_id, sku_id | Ulta's product id (pimprod2057355, mixed case kept as sent) and the default SKU id. product_id is the unique key. Ulta groups the shades and sizes of a product under one id, so a row is a product, not a shade. |
name, brand | As the page shows them. brand is null if the page sends none. |
url, image_url | The product link (it carries ?sku= for the default SKU) and the photo, as sent. |
list_price, list_price_max | The list price. Most products have one price; when the shades or sizes differ the page shows a range ($27.50 - $55.00), and then list_price is the low end and list_price_max the high end. list_price_max is null for a single price, never equal to list_price. |
sale_price, sale_price_max | The current price when marked down, split into low and high the same way. Null when not on sale, never 0. |
on_sale | True when the page sent a sale price. Ulta sends one when some version of the product is cheaper than its list price. For a product with a price range the low ends can be equal (a $4.49 - $10.00 product on sale at $4.49 - $5.00 was read on 2026-10-19), so compare the high ends too. No sale price read was above its list price. |
discount_amount | The dollars off the page shows ($10.00 is 10.0). For a product with a price range this is the page's own figure for the group and need not equal list_price minus sale_price. Null when the page shows none, which includes some products that are on sale. |
promo_text | The card's promotion line (Free Gift with purchase, Bundle & Save!, 5 for $10, Replenish & Save 20%). Null if none. |
rating, review_count | The average star rating (1 to 5) and how many reviews it is from. Both are null for a product with no reviews: the page sends null, which is not a rating of 0. |
badges | The card's badges in the order shown (Online only, Only at Ulta, New, Sale). Empty list if none. |
variant_label | How many shades, sizes or scents the product groups (57 colors, 3 sizes). Null for a single version. |
limited_stock, sponsored | The page's limited-stock flag and paid-placement flag. Null if absent. |
currency | USD. |
Example row (real values, 2026-10-19):
{"source": "/shop/makeup/face/foundation","position": 2,"product_id": "xlsImpprod10791919","sku_id": "2206255","name": "Even Better Makeup Broad Spectrum SPF 15 Foundation with Vitamin C","brand": "Clinique","url": "https://www.ulta.com/p/even-better-makeup-broad-spectrum-spf-15-foundation-with-vitamin-c-xlsImpprod10791919?sku=2206255","image_url": "https://media.ultainc.com/i/ulta/2206255","currency": "USD","list_price": 39.0,"list_price_max": null,"sale_price": 29.0,"sale_price_max": null,"on_sale": true,"discount_amount": 10.0,"promo_text": "Bundle & Save!","rating": 4.4,"review_count": 4159,"badges": ["Sale"],"variant_label": "50 colors","limited_stock": false,"sponsored": false}
Honest nulls
A field the page did not send is null, never 0 or "". badges is an empty
list when there are none. Measured on 2026-10-19, of 708 products read:
94 (13%) had no reviews (rating and review_count null) and 24 were on sale.
- No per-shade rows or prices. The list carries one price (or one range) per
product.
variant_labelsays how many shades it groups. - No stock count, description or ingredients. This is the category and brand list, one row per product.
- No percent off. The page shows the discount in dollars only; compute a
percent from
list_priceandsale_pricefor a single-price product.
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.00028 per product row written (
product-scraped), i.e. $0.28 per 1,000 products. A row that is not written is not charged: repeats of a product another page already returned are free.
How it fetches
It reads the product list that ulta.com's own listing pages embed in their page
state (window.__APOLLO_STATE__). Measured 2026-10-19: HTTP 200 HTML (1.2 to
4.5 MB a page) from a plain home connection and from Apify datacenter and
residential addresses (14 of 15 pages on a datacenter exit in the pick step), no
key or cookie needed. A refusal (403, 429, 5xx), a timeout, a 200 that carries
no page data (Ulta's small "Our Apologies" waiting-room page), a page other than
the one asked for, 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:
- Paging uses
page=, 1-based, 64 products a page. Foundation has 253 products over four pages (64, 64, 64, 61), disjoint; Maybelline has 98 over two. A page past the end is HTTP 200 with no products, and the actor stops there. The page says which page it rendered, and the actor treats a mismatch as a refusal rather than return the first page again and charge for it. - The total is the page's own product count (253 for foundation). It is not
taken from the page title, which omits it on some listings (has 3,886 products and no count in its title). It drifts with Ulta's stock.New Beauty Products
- A trailing slash loses the page.
/shop/makeup/face/foundation/?page=2is redirected to page 1; the actor always sends the path without one. - Depth. At most 10,000 products per page are read. The largest listing checked, New Beauty Products (3,886 products), answers 61 pages (60 full pages of 64, then 46) and an empty page 62; moisturizers (2,154) ends at page 34.
- Ulta can sort with a
sortparameter, but the values were not mapped, so none is offered and a query string in your URL is ignored. Sort the dataset bysale_priceordiscount_amountinstead. - A path that does not exist is HTTP 404 (
/brand/fenty-beauty) or Ulta's waiting-room page (/shop/sale); the second is retried from fresh addresses before the page is given up on.
If Ulta changes its page data, the run fails with a message instead of returning empty rows.