What the Sleep Number scraper collects
Reads sleepnumber.com category, collection and product pages and writes one row per size variant: sku, price, original price, discount, rating, review count and availability. The data comes from the page's own server-rendered hydration payload, with prices in exact integer cents, to JSON and CSV.
Category or collection listing
Input: A category or collection slugOne row per size variant of every product on the listing, with per-size price, sale price, rating and availability.
Product detail
Input: A product's URL slugThe same rows for one model: every size it is sold in.
From zero to Sleep Number data in 3 steps
Install
Clone the repository and install one browser engine. Python 3.9+.
$ git clone https://github.com/2scraper/sleepnumber-scraper.git && cd sleepnumber-scraper $ pip install -r requirements.txt -r requirements-playwright.txt $ playwright install chromium $ python3 playwright_scraper.py --category mattresses --format both --out mattresses
Run a scraper
Pick what you need and run it. Every flag is documented in the README.
$ python3 playwright_scraper.py --category mattresses --format both --out mattresses$ python3 playwright_scraper.py --mode product --category cm-mattressGet structured data
Rows land in JSON or CSV (
--format), with a.meta.jsonsidecar recording status, pages fetched and why the run stopped.
Sleep Number data fields
24 columns per row in a predictable schema. The most useful ones:
| Field | What it holds |
|---|---|
sku | Size-variant id, e.g. QCM10 for a Queen ComfortMode |
title | Product name |
size | Size of this variant (Twin, Queen, King…) |
price | Current (sale) price, as a number |
currency | ISO currency code from the site's own data (USD) |
original_price | Regular price, only when genuinely higher than price |
discount_pct | Discount computed from price and original_price |
promo_badge | The promotion text the site shows |
rating | Average review rating; null when unreviewed |
review_count | Number of reviews; null when unreviewed |
in_stock | Availability flag |
product_type | mattress, base, bedding, furniture… |
category | Category name |
product_slug | The model's slug |
price_source | Which view the price came from, and whether JSON-LD confirmed it |
url | Variant URL on www.sleepnumber.com |
[
{
"source": "sleepnumber.com",
"scraped_at": "2026-09-17T15:08:07.263728+00:00",
"url": "https://www.sleepnumber.com/collections/mattresses-comfortmode-collection/TCM10",
"sku": "TCM10",
"title": "ComfortMode™ Mattress",
"brand": "Sleep Number",
"price": 989.1,
"currency": "USD",
"original_price": 1099.0,
"discount_pct": 10.0,
"price_source": "payload",
"rating": 4.7222,
"review_count": 180,
"in_stock": true,
"image_url": "https://cdn.sleepnumber.com/image/upload/f_auto,q_auto:eco/v1773780981/workarea/catalog…",
"category": "Mattresses",
"size": "Twin",
"product_id": "AF1E5D37DF",
"product_slug": "comfortmode",
"collection_slug": "mattresses-comfortmode-collection",
"product_type": "mattress",
"promo_badge": "$109.90 OFF",
"page": 1,
"position": 1
},
{
"source": "sleepnumber.com",
"scraped_at": "2026-09-17T15:08:07.263756+00:00",
"url": "https://www.sleepnumber.com/collections/mattresses-comfortmode-collection/XCM10",
"sku": "XCM10",
"title": "ComfortMode™ Mattress",
"brand": "Sleep Number",
"price": 1079.1,
"currency": "USD",
"original_price": 1199.0,
"discount_pct": 10.0,
"price_source": "payload",
"rating": 4.7222,
"review_count": 180,
"in_stock": true,
"image_url": "https://cdn.sleepnumber.com/image/upload/f_auto,q_auto:eco/v1773780981/workarea/catalog…",
"category": "Mattresses",
"size": "Twin XL",
"product_id": "AF1E5D37DF",
"product_slug": "comfortmode",
"collection_slug": "mattresses-comfortmode-collection",
"product_type": "mattress",
"promo_badge": "$119.90 OFF",
"page": 1,
"position": 2
}
]First 2 of 9 rows from a real run — sample_output.json. Long values shortened here.
Handles what breaks naive Sleep Number scrapers
One row per size
ComfortMode spans seven sizes and seven prices. Each size is its own row keyed on its own sku, so diff_runs.py reports a Queen going on sale while the King does not.
Reads the hydration payload, not the paint
The product grid is painted client-side. The scraper decodes React Router's turbo-stream payload, which carries every product, variant and price in integer cents before a script runs.
No phantom discounts
The site's regular price equals its sale price on most items. original_price stays null unless the regular price is genuinely higher, so the file is not full of 0% discounts.
Landing pages and refusals named correctly
A curated /categories/ page with no product list reports no_listing, a CloudFront refusal reports blocked, and the site's 200-KB 404 page is told apart by its route data — none of them pass as "zero products".
Scraping infrastructure included
The same tested core runs every 2scraper repository, so the hard parts work the same way on every site.
CAPTCHA handling
Detects the challenge a page really serves and, with a 2Captcha key, solves it only when it blocks the data — a capped number of solves per page.
Proxy rotation
Single proxy or a pool from a file, rotated on blocks with a fresh browser per exit. Credentials stay out of argv and logs.
Fingerprints
Optional complete device identity from the 2Captcha Fingerprint API — user agent, client hints, locale and timezone that agree with each other.
Honest run metadata
Every run writes a .meta.json sidecar and distinct exit codes: complete, blocked, empty and partial are never confused, and a failed run never overwrites good data.
Diff two runs
diff_runs.py compares two complete Sleep Number snapshots by id — new, removed and changed rows for monitoring.
What teams do with Sleep Number data
Mattress price monitoring
Track every size's price and sale price over time and catch per-size promotions.
Promotion tracking
Record promo badges and computed discounts across beds, bases, bedding and furniture.
Competitive benchmarking
Compare Sleep Number's per-size pricing with other bed and mattress retailers.
Full-catalogue snapshots
The sellable catalogue is small — 88 products in the sitemap when measured — so a complete pass takes minutes.
Free to run. Pay only for what the site requires.
Free to run · paid add-ons optional
No key, captcha solving or fingerprint is needed, and from a home connection the scraper needs nothing at all. Every datacentre address — VPS, CI runner, container — gets a CloudFront 403 refusal with no challenge on it, so from a server you need a residential proxy or the Scraping Browser API, which brings its own residential exit. Any residential exit country returns the same USD prices.
Optional 2Captcha services — captcha solving, the Scraping Browser API, residential proxies and fingerprints — work behind one key.
Good to know
- Selenium cannot authenticate a password proxy, so on this site it needs a residential connection, an IP-authenticated proxy or a local forwarding proxy.
- The Scraper API's own exits are refused; it works here only routed through a Scraping Browser session (--cdp-url).
- rating and review_count are null on unreviewed products.
2scraper vs. hosted scraper APIs
| 2scraper | Typical hosted API | |
|---|---|---|
| Code | Open source, MIT — read and patch it | Closed |
| Cost | Free; optional services billed only when used | Subscription or per request / per result |
| Runs on | Your machine, server or CI | The vendor's infrastructure |
| Output | JSON/CSV files + run metadata | API responses |
| When the site changes | Fix it yourself or open an issue / PR | Wait for the vendor |
Sleep Number scraper FAQ
Why does it get HTTP 403 from my server?
Sleep Number refuses datacentre addresses outright. Run it from a residential connection, or set a residential proxy in .env, or use the Scraping Browser API.
Do I need captcha solving?
No. Nothing on this site renders a challenge; what helps against the refusal is a different exit address.
Does the proxy country matter?
No. The same category through GB and US residential exits returned the same skus with identical USD prices. Pin a region only if you want a reproducible exit.
Why one row per size instead of per product?
Each size has its own sku and its own price, and that size dimension is what a mattress price monitor most needs.
How does pagination work?
?page=N is ignored by the site, so pages are planned from the payload's own arithmetic. Every category measured fits on one page of 100, and --concurrency is refused.
Is it legal to scrape Sleep Number?
It reads public catalogue pages. Respect robots.txt, the site's terms, the law in your jurisdiction and a sane request rate.