How to Scrape Temu Product and Price Data in 2026: Proxies, Anti-Bot, and the Hidden JSON

Temu has no public API. This guide covers the HTML-vs-JSON trade-off, Cloudflare Turnstile bypass with residential proxies, and a full Python implementation using curl_cffi and ProxyHat to extract product and price data reliably.

How to Scrape Temu Product and Price Data in 2026: Proxies, Anti-Bot, and the Hidden JSON

If you need to scrape Temu product and price data in 2026, you face an immediate architectural decision: parse the rendered HTML of listing and product pages, or reverse-engineer the internal JSON endpoints that power Temu's Next.js storefront. Both paths work, but they trade stability against complexity in very different ways — and getting that choice wrong will cost you days of broken selectors or instant IP bans.

Temu operates one of the most aggressive anti-bot stacks in e-commerce. Datacenter IPs are flagged within the first request. Prices, shipping costs, and even product availability shift by geographic region. And the HTML you parse today may use different hashed class names tomorrow. This guide walks through the real trade-offs, the anti-bot technology you need to circumvent, and a working Python implementation using curl_cffi with ProxyHat residential proxies.

Scrape Temu: The API-vs-HTML Trade-off

Temu has no public API for catalog browsing, search, or product details. The storefront at temu.com is a server-rendered Next.js application. When a browser loads a product page, the server embeds a large JSON state blob (similar to __NEXT_DATA__) inside a <script> tag, then hydrates the React tree. Subsequent interactions — search, pagination, cart updates — hit internal endpoints like /api/poppy/v1/search and goods-detail routes that return clean JSON.

That gives you two extraction strategies:

ApproachStabilityComplexityData RichnessAnti-Bot Exposure
HTML parsing (DOM selectors)Low — hashed classes change per deployLow — CSS/XPath selectors onlyMedium — only visible DOM fieldsHigh — every page load hits Cloudflare
Internal JSON API endpointsHigh — endpoint paths are stableHigh — signed headers, anti_content tokenHigh — full SKU, shipping, review dataMedium — same CDN but fewer requests needed

For most price-intelligence use cases, the JSON endpoints are the better target. You get structured data (SKU lists, sale prices, shipping estimates, review counts) without brittle CSS selectors. The cost is that you need to reverse-engineer the anti_content header — a signed token that Temu's frontend JavaScript generates and includes in every API request. Without it, the API returns a 403 or an anti-bot challenge page.

For one-off or small-scale extraction, parsing the embedded state blob from the initial HTML response is a pragmatic middle ground. You get JSON without needing to generate the anti_content token, because the server embeds it server-side before Cloudflare's challenge layer applies to subsequent XHR calls.

Temu's Anti-Bot Stack: What You're Up Against

Temu sits behind Cloudflare's bot management infrastructure, which includes Turnstile challenges, TLS fingerprinting, and HTTP/2 connection analysis. Here's what each layer does and how it affects your scraper:

Cloudflare Turnstile and Bot Score

Cloudflare assigns every incoming request a bot score based on TLS fingerprint, HTTP/2 settings (like SETTINGS_HEADER_TABLE_SIZE and WINDOW_UPDATE patterns), header order, and IP reputation. Datacenter IP ranges are scored near-zero trust. Residential IPs from real ISPs score significantly higher, which is why a datacenter proxy will get challenged or blocked on the first request while a residential proxy often passes without any visible challenge.

Turnstile itself is Cloudflare's managed challenge widget — similar to reCAPTCHA but invisible to most real users. When the bot score drops below a threshold, Cloudflare serves a JavaScript challenge page instead of the requested content. Your scraper receives HTML containing a challenge script, not product data.

The Signed anti_content Header

Beyond Cloudflare, Temu adds its own application-layer protection. Every API call to /api/poppy/v1/* endpoints includes an anti_content parameter (sometimes passed as a header, sometimes in the POST body). This token is generated by obfuscated JavaScript running in the browser. It encodes browser environment data — canvas fingerprint, WebGL renderer string, screen dimensions, timing information — and signs it with a key embedded in the page's JavaScript bundle.

Reverse-engineering this token is possible but fragile. The signing algorithm changes with each deploy of Temu's frontend. Most successful scrapers either:

  • Extract the embedded __NEXT_DATA__ blob from the initial HTML (no anti_content needed for server-rendered data)
  • Use a headless browser (Playwright, Puppeteer) to let the real JavaScript generate the token, then intercept the XHR calls
  • Run the obfuscated JS in a JavaScript runtime (Node.js with DOM polyfills) to generate tokens on demand

TLS and HTTP/2 Fingerprinting

Even with the right headers and a valid anti_content token, Cloudflare inspects the TLS ClientHello and HTTP/2 SETTINGS frame to fingerprint your HTTP client. Python's standard requests library uses urllib3's TLS stack, which has a fingerprint that does not match any real browser. Cloudflare detects this instantly.

This is why ProxyHat recommends curl_cffi — a Python library that wraps libcurl with BoringSSL and can impersonate Chrome, Firefox, or Safari TLS signatures. When you set impersonate="chrome", curl_cffi sends a ClientHello that matches Chrome's exact cipher suite ordering, extensions, and ALPN protocols. Combined with a residential proxy, this is the most reliable way to pass Cloudflare's bot score threshold.

Locating Data: Embedded State and Hidden JSON Endpoints

Temu's product pages embed a large JSON blob in a script tag. The exact attribute varies between deploys, but it typically follows the Next.js __NEXT_DATA__ pattern or a custom id attribute. Here's how to find and extract it:

The Embedded State Blob

When you fetch a product page like https://www.temu.com/products/goods-detail.html?goods_id=123456, the HTML response contains a script tag with type application/json. The structure looks roughly like:

// Truncated — real blobs are 50-200 KB per product
{
  "props": {
    "pageProps": {
      "goods": {
        "goods_id": "123456",
        "goods_name": "Wireless Earbuds Pro Max",
        "goods_price": {
          "normal_price": {"amount": 12.98, "currency_code": "USD"},
          "sale_price": {"amount": 8.99, "currency_code": "USD"}
        },
        "sku_list": [
          {"sku_id": "sku_001", "spec": "Black", "price": 8.99},
          {"sku_id": "sku_002", "spec": "White", "price": 9.49}
        ],
        "shipping_info": {"free_shipping": true, "estimated_days": 7}
      }
    }
  }
}

This blob contains everything visible on the page: product title, price (both list and sale), SKU variants, image URLs, shipping estimates, and review summaries. You don't need the anti_content token to get it — it's server-rendered.

Hashed CSS Classes and data-uniqid Attributes

If you fall back to HTML parsing, you'll notice that Temu's class names are hashed — strings like _23f9a1c that change with every frontend deploy. This makes traditional CSS selectors unreliable. Instead, look for:

  • data-uniqid attributes on product cards — these are stable identifiers like data-uniqid="goods_card_price"
  • Structured aria-label and itemprop attributes that survive class hashing
  • Text-based XPath selectors that match on label content rather than class names

Internal JSON Endpoints

For search and pagination, the frontend calls /api/poppy/v1/search with a POST body containing the keyword, page number, and the anti_content token. The response is a JSON object with a data.list array of product summaries. Each summary includes goods_id, goods_name, goods_price, and a thumbnail URL.

Product detail data comes from a similar endpoint — often /api/poppy/v1/goods/detail — which returns the same structure as the embedded blob but with additional fields like full SKU matrices and shipping options for your geo region.

Rate Limits, Geo-Targeting, and Why Residential Proxies Are Mandatory

Temu enforces aggressive rate limits that are layered on top of Cloudflare's bot management. While exact thresholds are not publicly documented, empirical testing shows that:

  • A single datacenter IP gets challenged after roughly 3-5 requests within a minute
  • A residential IP can typically sustain 20-40 requests per minute before seeing intermittent 429 responses
  • With rotating residential proxies, you can push 100-200 concurrent sessions across a pool, though we recommend staying under 150 to maintain a >95% success rate

More importantly, prices and shipping estimates vary by geographic region. A product that costs $8.99 with free 7-day shipping in the US may show $12.50 with $2.99 shipping in Germany. If your price intelligence pipeline scrapes from a single IP location, you'll capture regional pricing that doesn't reflect the markets you care about.

This is where city-level geo-targeting becomes essential. ProxyHat lets you specify country and city in the username string:

# US residential IP
http://user-country-US:pass@gate.proxyhat.com:8080

# German residential IP in Berlin
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080

# US residential IP in a specific city
http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080

By distributing requests across multiple regions, you capture geo-specific pricing data and avoid triggering rate limits on any single IP. For a price monitoring job that tracks 500 SKUs across 5 markets, you'd rotate through 5 country-targeted proxy sessions, each handling ~100 requests per minute.

See ProxyHat's proxy locations for the full list of supported countries and cities, and check ProxyHat pricing for residential proxy plans that fit your concurrency needs.

Python Implementation: curl_cffi + ProxyHat

Here's a working example that fetches a Temu product page through a ProxyHat residential proxy, impersonates Chrome's TLS fingerprint, and extracts the embedded JSON state blob.

Installation

pip install curl_cffi

Fetching a Product Page

from curl_cffi import requests
import re
import json

# ProxyHat residential proxy — US geo for US pricing
proxy_url = "http://user-country-US:pass@gate.proxyhat.com:8080"

session = requests.Session(impersonate="chrome")

response = session.get(
    "https://www.temu.com/products/goods-detail.html",
    params={"goods_id": "123456"},
    proxies={"http": proxy_url, "https": proxy_url},
    timeout=30,
)

print(f"Status: {response.status_code}")
print(f"Response size: {len(response.text)} bytes")

Extracting the Embedded JSON

def extract_product_data(html: str) -> dict:
    """Parse the embedded Next.js state blob from a Temu product page."""
    # The script tag may use __NEXT_DATA__ or a custom id
    patterns = [
        r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>',
        r'<script type="application/json" id="__NEXT_DATA__">(.*?)</script>',
        r'window\.__INITIAL_STATE__\s*=\s*({.*?});',
    ]
    for pattern in patterns:
        match = re.search(pattern, html, re.DOTALL)
        if match:
            return json.loads(match.group(1))
    raise ValueError("Could not find embedded JSON state blob")

data = extract_product_data(response.text)
goods = data["props"]["pageProps"]["goods"]

product = {
    "goods_id": goods["goods_id"],
    "title": goods["goods_name"],
    "sale_price": goods["goods_price"]["sale_price"]["amount"],
    "list_price": goods["goods_price"]["normal_price"]["amount"],
    "currency": goods["goods_price"]["sale_price"]["currency_code"],
    "sku_count": len(goods.get("sku_list", [])),
    "free_shipping": goods.get("shipping_info", {}).get("free_shipping", False),
}

print(json.dumps(product, indent=2))

Sample output (truncated):

{
  "goods_id": "123456",
  "title": "Wireless Earbuds Pro Max",
  "sale_price": 8.99,
  "list_price": 12.98,
  "currency": "USD",
  "sku_count": 4,
  "free_shipping": true
}

Using curl for Quick Testing

curl -x http://user-country-US:pass@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" \
  -H "Accept: text/html,application/xhtml+xml" \
  -H "Accept-Language: en-US,en;q=0.9" \
  "https://www.temu.com/products/goods-detail.html?goods_id=123456"

Note: plain curl won't impersonate Chrome's TLS fingerprint. Use it for quick connectivity tests against the proxy, but switch to curl_cffi for production scraping.

Sticky Sessions, Retries, and Pagination

Sticky Sessions for Cart and Shipping Consistency

When you scrape multiple pages of a product or follow a search-to-detail flow, you need the same IP across requests. If your IP rotates mid-session, Temu's backend may detect the inconsistency (different geo, different TLS fingerprint hash) and trigger a challenge.

ProxyHat supports sticky sessions via the -session- flag in the username:

import uuid

session_id = f"temu-{uuid.uuid4().hex[:8]}"
proxy_url = f"http://user-country-US-session-{session_id}:pass@gate.proxyhat.com:8080"

# All requests through this proxy_url use the same residential IP
# for up to 10 minutes or until the session expires

Use sticky sessions when:

  • Following a search-to-product-detail flow (same user browsing pattern)
  • Checking cart/shipping estimates (requires geo consistency)
  • Scraping paginated results where the backend tracks your browse session

Retry Logic with Backoff

import time
import random

def fetch_with_retry(url, params, proxy, max_retries=3):
    for attempt in range(max_retries):
        try:
            response = session.get(
                url,
                params=params,
                proxies={"http": proxy, "https": proxy},
                timeout=30,
            )
            if response.status_code == 200:
                return response
            elif response.status_code == 429:
                # Rate limited — back off with jitter
                wait = (2 ** attempt) + random.uniform(1, 5)
                print(f"429 on attempt {attempt+1}, waiting {wait:.1f}s")
                time.sleep(wait)
            elif response.status_code == 403:
                # Cloudflare challenge — rotate IP
                print(f"403 on attempt {attempt+1}, rotating proxy")
                proxy = f"http://user-country-US-session-{uuid.uuid4().hex[:8]}:pass@gate.proxyhat.com:8080"
                time.sleep(2)
        except Exception as e:
            print(f"Error on attempt {attempt+1}: {e}")
            time.sleep(2 ** attempt)
    return None

Pagination Through Search Results

def scrape_search_results(keyword: str, max_pages: int = 5):
    all_products = []
    for page in range(1, max_pages + 1):
        # Rotate session per page to avoid rate limits
        sid = f"search-{keyword[:10]}-p{page}-{uuid.uuid4().hex[:6]}"
        proxy = f"http://user-country-US-session-{sid}:pass@gate.proxyhat.com:8080"

        response = fetch_with_retry(
            "https://www.temu.com/search-result.html",
            params={"search_key": keyword, "page": page},
            proxy=proxy,
        )
        if not response:
            break

        data = extract_product_data(response.text)
        items = data.get("props", {}).get("pageProps", {}).get("searchResult", {}).get("list", [])
        if not items:
            break

        for item in items:
            all_products.append({
                "goods_id": item.get("goods_id"),
                "title": item.get("goods_name"),
                "price": item.get("goods_price", {}).get("sale_price", {}).get("amount"),
            })

        # Respectful delay between pages
        time.sleep(random.uniform(2, 5))

    return all_products

For broader scraping strategies and best practices, see our web scraping use case guide and SERP tracking overview.

Ethics, Terms of Service, and Legal Boundaries

Scraping public catalog data — product titles, prices, images, and shipping estimates visible without logging in — is generally lower risk than scraping behind authentication. However, you should understand the legal landscape before building a production pipeline.

What's Generally Acceptable

  • Scraping publicly visible product pages without logging in
  • Respecting robots.txt directives (check temu.com/robots.txt before crawling)
  • Rate-limiting your requests to avoid degrading the site for real users
  • Storing only catalog data (prices, titles, SKUs) — not personal data

What to Avoid

  • Scraping behind login (user accounts, order history, saved addresses) — this likely violates Temu's Terms of Service and may implicate the Computer Fraud and Abuse Act (CFAA) in the US
  • Scraping personal data (reviews tied to user profiles, shipping addresses) without considering GDPR/CCPA obligations
  • Reselling scraped data in violation of Temu's ToS
  • Overwhelming the site with aggressive concurrency that degrades service for real users

When to Use the Official Merchant Feed Instead

If you're a Temu seller or authorized partner, Temu offers a merchant API through their seller dashboard that provides product, order, and inventory data without scraping. This is always the preferred route when available — it's stable, legal, and doesn't risk IP blocks. Use web scraping only for competitive price intelligence where no API access exists.

Key Takeaways

The JSON endpoints are more stable than HTML selectors. Temu's hashed CSS classes change every deploy, but the /api/poppy/v1/* endpoint paths and the embedded __NEXT_DATA__ blob structure are relatively stable. Prefer JSON extraction over DOM parsing.

  • Residential proxies are non-negotiable. Datacenter IPs are blocked within 3-5 requests. Residential IPs with city-level geo-targeting let you capture region-specific pricing and sustain 20-40 requests per minute per IP.
  • Use curl_cffi with impersonate="chrome" to match Chrome's TLS/HTTP2 fingerprint. Standard requests will be detected by Cloudflare immediately.
  • Geo-targeting matters for price data. Prices and shipping vary by country and sometimes by city. Use -country-US or -country-DE-city-berlin flags in your ProxyHat username to capture market-specific data.
  • Sticky sessions for multi-step flows. Use -session-{id} to maintain IP consistency when following search-to-detail paths or checking shipping estimates.
  • Respect ToS and legal boundaries. Scrape only public catalog data. Avoid authenticated endpoints. Check robots.txt. Use the official merchant API if you have seller access.

Ready to start scraping? Explore ProxyHat residential proxy plans or read the ProxyHat documentation for full API reference and advanced configuration.

Ready to get started?

Access 50M+ residential IPs across 148+ countries with AI-powered filtering.

View PricingResidential Proxies
← Back to Blog