Ulta Beauty Scraper - Beauty Price & Sale Tracker (USA) avatar

Ulta Beauty Scraper - Beauty Price & Sale Tracker (USA)

Pricing

from $0.28 / 1,000 product scrapeds

Go to Apify Store
Ulta Beauty Scraper - Beauty Price & Sale Tracker (USA)

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

Superslow Sloth

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

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

FieldNotes
startUrlsOne 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.
maxItemsProducts to read per page (default 100, up to 10,000). 64 per page.
proxyConfigurationApify 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).

FieldNotes
source, positionThe 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_idUlta'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, brandAs the page shows them. brand is null if the page sends none.
url, image_urlThe product link (it carries ?sku= for the default SKU) and the photo, as sent.
list_price, list_price_maxThe 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_maxThe current price when marked down, split into low and high the same way. Null when not on sale, never 0.
on_saleTrue 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_amountThe 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_textThe card's promotion line (Free Gift with purchase, Bundle & Save!, 5 for $10, Replenish & Save 20%). Null if none.
rating, review_countThe 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.
badgesThe card's badges in the order shown (Online only, Only at Ulta, New, Sale). Empty list if none.
variant_labelHow many shades, sizes or scents the product groups (57 colors, 3 sizes). Null for a single version.
limited_stock, sponsoredThe page's limited-stock flag and paid-placement flag. Null if absent.
currencyUSD.

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_label says 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_price and sale_price for 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 (
    New Beauty Products
    has 3,886 products and no count in its title). It drifts with Ulta's stock.
  • A trailing slash loses the page. /shop/makeup/face/foundation/?page=2 is 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 sort parameter, but the values were not mapped, so none is offered and a query string in your URL is ignored. Sort the dataset by sale_price or discount_amount instead.
  • 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.