Price IntelligenceElite tier

Monitoring Event Ticket Prices and Availability Across Ticketmaster, StubHub, and SeatGeek

Ticket pricing is one of the fastest-moving public datasets on the web: face-value inventory on Ticketmaster and resale listings on StubHub and SeatGeek reprice by the minute as an event approaches, and every price is geo- and event-specific. There is no unified public price API across these marketplaces, so building a price-intelligence or consumer-comparison view means reading each site's public listing pages and internal JSON endpoints directly. This guide covers how to collect accurate public price and availability data at volume - defensively, respectfully, and strictly for comparison and analytics, never for buying.

ARWritten by Alex Rivera·Reviewed by J Shirisha·Updated September 4, 2026
13 min read
Editorial board contrasting public ticket listing prices with a blocked checkout and queue path.

Key takeaways

  • This guide is about reading PUBLIC listing prices and availability only. Automated ticket PURCHASING, queue/waiting-room bypass, and purchase-limit evasion are out of scope and illegal in many jurisdictions (e.g. the US BOTS Act of 2016).
  • Ticketmaster blocks on reputation, TLS/JA3 fingerprint, and behavioral signals - not just request rate. A clean datacenter IP with a perfect header set still fails; it is the combination that gets flagged.
  • Prices are event- and geo-specific: the same event shows different inventory and currency depending on the exit IP's country, so you must pin geography deliberately rather than take whatever exit you land on.
  • StubHub and SeatGeek expose structured JSON behind their event pages (listing feeds and grid-price endpoints) - parsing those is far more stable than scraping rendered HTML, but they rotate paths and require the right referer and tokens.
  • A 200 OK is not proof of good data. Soft blocks, empty listing arrays, and stale cached prices all return 200 - assert on inventory shape and freshness before you trust a row.

On this page

What goes wrong when this scraper fails

Anyone building price intelligence, a consumer price-comparison tool, or resale-market research needs to see the same public prices a fan sees: face-value availability on the primary marketplace (Ticketmaster) and resale asks on the secondaries (StubHub, SeatGeek). The problem is that no single public API spans all three, prices are dynamic and change fast in the final days before an event, and each site treats its listing pages as a heavily defended surface. You end up polling many events across many geographies on a tight cadence, which is exactly the traffic shape these sites are built to throttle.

Why this failure mode happens

Ticketmaster runs Queue-it / Akamai-style waiting rooms plus aggressive bot management (device fingerprinting, TLS/JA3 checks, PerimeterX-style challenges) because scalping bots have historically hammered its purchase path - so its defenses are tuned for hostility even against read-only traffic. StubHub and SeatGeek are lighter but still front their listing JSON with Cloudflare, rotate endpoint paths, and rate-limit per IP. On top of the anti-bot layer, prices are genuinely dynamic: primary inventory releases in waves, resale sellers reprice against each other, and everything is localized by country and currency - so a single fixed IP gives you a narrow, often stale window into a much larger, faster-moving market.

Three columns for Ticketmaster, StubHub, and SeatGeek public listing feeds.
Secondaries often expose listing JSON. Primary is a defended HTML page.

Challenges that make this hard to automate

  • Ticketmaster's layered defense: Akamai/PerimeterX device fingerprinting plus Queue-it waiting rooms that intercept even read requests to hot events, returning challenge pages instead of price data.
  • Geo-locked, currency-localized pricing: the same event URL returns different inventory, availability, and currency by exit-IP country, so undirected exits produce inconsistent, non-comparable rows.
  • Endpoint churn on the secondaries: StubHub and SeatGeek rotate the internal JSON paths and query params their front-ends call, so a hardcoded endpoint quietly breaks and starts returning 404s or empty arrays.
  • Freshness vs. volume: prices near an event move within minutes, but polling thousands of events fast enough to stay fresh is exactly the velocity pattern that trips per-IP rate limits.
  • Silent soft-blocks: all three sites can return 200 OK with an empty or truncated listing set (or a stale cached page) instead of an outright 403, so bad data slips into your store looking valid.

Approaches that usually fail

  • Single datacenter IP with a scraper library: works for a handful of low-traffic events, then gets rate-limited or CAPTCHA-walled within minutes on Ticketmaster and Cloudflare-fronted secondaries.
  • Third-party 'ticket data' aggregator APIs: convenient but expensive, often stale, incomplete across all three marketplaces, and a black box you cannot verify against the live public page.
  • Headless-browser farms without proxy geography control: burn huge CPU rendering pages and still get blocked because every browser exits from the same flagged IP range, and they cannot pin per-event country context.
  • Manual spot-checks: a person opening event pages a few times a day - accurate but hopelessly unscalable and blind to the intra-hour price swings that matter most near an event.

When residential proxies fix this — and when they cannot

Rotating residential proxies give every request a fresh, real consumer IP, so consecutive polls of a hot event never stack on one address and trip velocity limits. Because you choose the exit country, you can pin each request to the geography that matters for that event - collecting the exact localized inventory, availability, and currency a real fan in that market would see, which is what makes prices comparable across marketplaces. Sticky sessions let a short multi-request burst (load event page, then fetch its listing JSON with the matching referer and tokens) stay on one exit so the site sees a coherent session, while per-request rotation remains the default for independent price checks.

Two panes showing HTTP 200 with an empty inventory versus packed listing rows with prices.
Assert inventory shape and freshness before you trust a ticket row.

How Aethyn residential proxies help here

Aethyn is a residential and mobile proxy network built for exactly this kind of high-cadence, geo-specific public-data collection. You address a single endpoint - proxy.aethyn.io - and encode rotation, country, city, and session behavior directly in the username, so the same code handles a per-request price sweep and a sticky page+JSON burst with one line changed. On the Elite tier you get the cleanest residential pools and per-request rotation on port 5499, which is what keeps you under Ticketmaster's reputation and velocity radar.

  • Per-request rotation by default on port 5499 (Elite) so every Ticketmaster/StubHub/SeatGeek poll exits from a fresh residential IP - no manual IP pool management.
  • Precise geo-pinning via -country-XX (and optional -city-name) so you collect the correct localized inventory, availability, and currency for each event's market.
  • Sticky sessions (-session-TOKEN-lifetime-MINUTES, up to 30 min) to keep a page-load-then-fetch-JSON burst on one coherent exit when the referer/token flow requires it.
  • Large residential and mobile pools that present as real consumer ISPs - the reputation signal Ticketmaster's Akamai/PerimeterX layer weighs most heavily.
  • One stable endpoint and username-encoded config, so scaling from 10 events to 10,000 is a concurrency change, not an infrastructure change.
  • Elite-tier exit quality tuned for the hardest anti-bot surfaces, reducing challenge/soft-block rates that otherwise poison your price data silently.

How to implement this with residential proxies

  1. 1

    Establish a geo-pinned Elite proxy connection and verify your exit

    Before touching any ticket site, confirm your proxy config resolves to the country you intend to collect from - a mismatched exit is the single most common cause of non-comparable price rows. Encode rotation and geography in the username: use the Elite port 5499, set the country, and leave the session token off so you rotate on every request. Hit an echo IP service first and assert the country matches, then move on.

    python
    import requests
    
    # Elite tier -> port 5499. Per-request rotation (no session token).
    PROXY = "http://aethyn-XXXXX-country-us:PASSWORD@proxy.aethyn.io:5499"
    proxies = {"http": PROXY, "https": PROXY}
    
    r = requests.get("https://ipinfo.io/json", proxies=proxies, timeout=20)
    info = r.json()
    print(info["ip"], info.get("country"))
    assert info.get("country") == "US", f"Wrong exit country: {info.get('country')}"
    

    Field note: Assert the exit country on every worker startup, not just once in dev. Residential pools are large and occasionally a request lands on a border-adjacent exit - failing loud here saves you from silently mixing US and CA inventory into the same price series.

  2. 2

    Collect Ticketmaster public availability with a complete, realistic request

    Ticketmaster is the most defended surface here - Akamai/PerimeterX fingerprinting plus Queue-it waiting rooms on hot events. Send a full, coherent header set (a bare User-Agent with no Accept-Language or Accept is a classic bot tell), rotate on every request, and detect the waiting-room/challenge response instead of parsing it as data. Read only public availability and price-range data exposed on the event's public discovery surface; do not touch the cart or purchase path.

    python
    import requests
    
    PROXY = "http://aethyn-XXXXX-country-us:PASSWORD@proxy.aethyn.io:5499"
    proxies = {"http": PROXY, "https": PROXY}
    
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                      "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "en-US,en;q=0.9",
        "Referer": "https://www.ticketmaster.com/",
    }
    
    # Public discovery/availability surface - read-only, no cart interaction.
    url = "https://www.ticketmaster.com/event/EVENT_ID"
    r = requests.get(url, headers=headers, proxies=proxies, timeout=30)
    
    body = r.text.lower()
    if r.status_code in (403, 429) or "queue-it" in body or "are you a robot" in body:
        raise RuntimeError(f"Blocked/queued (status={r.status_code}) - back off and re-rotate")
    
    print("OK", r.status_code, len(r.text))
    

    Field note: Treat any Queue-it redirect or challenge page as a hard signal to back off that event for several minutes, not to retry immediately. Hammering a waiting-room URL on a fresh IP each time just burns good residential exits against a door that is closed to everyone right now.

  3. 3

    Pull StubHub resale listings from the public JSON feed

    StubHub renders its listing grid from an internal JSON feed rather than static HTML, so parse the feed, not the DOM - it is far more stable and gives you per-listing price, quantity, and section. Load the public event page first, then request the listings JSON with the matching referer; a sticky session keeps both requests on one coherent exit. Localize by pinning the country so you get that market's currency and available inventory.

    python
    import requests
    
    # Sticky session so page-load + JSON fetch share one exit (lifetime in minutes).
    PROXY = "http://aethyn-XXXXX-country-us-session-sh42-lifetime-10:PASSWORD@proxy.aethyn.io:5499"
    proxies = {"http": PROXY, "https": PROXY}
    
    EVENT = "123456"
    s = requests.Session()
    s.proxies.update(proxies)
    s.headers.update({
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36",
        "Accept-Language": "en-US,en;q=0.9",
    })
    
    # 1) Warm the session on the public event page.
    s.get(f"https://www.stubhub.com/event/{EVENT}/", timeout=30)
    
    # 2) Fetch the public listings JSON with the event page as referer.
    listings = s.get(
        f"https://www.stubhub.com/api/events/{EVENT}/listings",
        headers={"Accept": "application/json",
                 "Referer": f"https://www.stubhub.com/event/{EVENT}/"},
        timeout=30,
    )
    data = listings.json()
    for lst in data.get("listings", [])[:5]:
        print(lst.get("section"), lst.get("row"), lst.get("price", {}).get("amount"))
    

    Field note: Cache the discovered JSON endpoint path per marketplace and health-check it separately from your parser. StubHub and SeatGeek rotate these internal paths every so often; when the feed starts returning 404 or an empty listings array on events you know are live, that is your cue to re-derive the path from the page's network calls, not a real sell-out.

  4. 4

    Pull SeatGeek grid prices and normalize across marketplaces

    SeatGeek exposes a public listings/grid endpoint per event. Collect its price, fee-inclusive total, and deal-score fields, then normalize all three marketplaces into one schema so a StubHub 'price' and a SeatGeek 'display price' actually mean the same thing. Fees and currency handling differ per site - capture them explicitly rather than assuming the headline number is comparable.

    python
    import requests
    
    PROXY = "http://aethyn-XXXXX-country-us:PASSWORD@proxy.aethyn.io:5499"
    proxies = {"http": PROXY, "https": PROXY}
    
    headers = {
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                      "(KHTML, like Gecko) Chrome/126.0 Safari/537.36",
        "Accept": "application/json",
        "Referer": "https://seatgeek.com/",
    }
    
    EVENT = "7654321"
    r = requests.get(f"https://seatgeek.com/api/event/{EVENT}/listings",
                     headers=headers, proxies=proxies, timeout=30)
    raw = r.json()
    
    def normalize(source, section, price, fees, currency):
        return {"source": source, "section": section,
                "base_price": price, "est_total": price + (fees or 0),
                "currency": currency}
    
    rows = [normalize("seatgeek", l.get("section"),
                      l.get("price"), l.get("fee_amount"), l.get("currency", "USD"))
            for l in raw.get("listings", [])]
    print(len(rows), "normalized SeatGeek rows")
    

    Field note: Store base price, estimated fees, and fee-inclusive total as separate columns - never just one 'price'. Ticketmaster face value, StubHub all-in resale, and SeatGeek deal-score pricing are only meaningfully comparable once fees and currency are explicit; collapsing them early is how a comparison view ends up misleading.

  5. 5

    Validate freshness and shape before writing anything

    A 200 OK proves the request completed, not that the data is good. Guard every row with structural and freshness checks: non-empty listing set, prices within a sane band, currency matching the pinned country, and a collected-at timestamp. Reject soft-blocks and stale cached pages here so they never reach your store. This gate is where price-intelligence pipelines earn their trust.

    python
    from datetime import datetime, timezone
    
    def validate(rows, expected_currency="USD"):
        if not rows:
            raise ValueError("Empty listing set - likely a soft block, not a sold-out event")
        prices = [r["base_price"] for r in rows if r.get("base_price")]
        if not prices:
            raise ValueError("No parseable prices - parser or endpoint drift")
        if min(prices) <= 0 or max(prices) > 100000:
            raise ValueError(f"Prices out of sane band: {min(prices)}..{max(prices)}")
        bad_ccy = [r for r in rows if r.get("currency") != expected_currency]
        if bad_ccy:
            raise ValueError(f"Currency mismatch - wrong exit geo? {bad_ccy[0]['currency']}")
        for r in rows:
            r["collected_at"] = datetime.now(timezone.utc).isoformat()
        return rows
    
    # clean = validate(rows, expected_currency="USD")
    # db.write(clean)
    

    Field note: Alert on a sudden drop in row count per event, not just on exceptions. A marketplace quietly returning 8 listings where it returned 400 yesterday usually means endpoint drift or a partial soft-block - catching that delta early is the difference between a data gap you notice and one you find three weeks later in a wrong dashboard.

  6. 6

    Schedule, throttle, and back off to stay fresh without hammering

    Near an event, prices move within minutes; far out, hourly is plenty. Tier your polling cadence by time-to-event so you spend velocity where it matters, and add jittered exponential backoff on any block signal. Keep per-event concurrency modest and let per-request rotation spread load across the residential pool rather than pushing one IP hard.

    python
    import time, random
    
    def cadence_seconds(days_to_event):
        if days_to_event <= 1:   return 300      # 5 min in the final day
        if days_to_event <= 7:   return 1800     # 30 min in the final week
        return 3600                              # hourly otherwise
    
    def fetch_with_backoff(fetch_fn, max_tries=4):
        delay = 5
        for attempt in range(max_tries):
            try:
                return fetch_fn()   # each call rotates to a fresh Elite exit
            except RuntimeError as e:  # block/queue signal from earlier steps
                sleep = delay + random.uniform(0, delay)  # jitter
                print(f"Backing off {sleep:.1f}s ({e})")
                time.sleep(sleep)
                delay *= 2
        raise RuntimeError("Giving up after repeated blocks - cool this event down")
    

    Field note: Jitter is not optional. A perfectly periodic poll every 300.0 seconds is itself a fingerprint; adding random slack makes your traffic look like organic checking and keeps you off velocity-based flags even when each individual request looks clean.

Best practices that keep scrapers reliable

  • Collect only public, non-personal data - listing prices, availability, section/row, and currency. Never scrape buyer or seller personal information, and always operate within each site's Terms of Service; respecting ToS and jurisdictional law is a hard requirement, not a nice-to-have.
  • Stay strictly read-only. This is price intelligence: never automate carting, checkout, queue/waiting-room bypass, or purchase-limit evasion - automated purchasing is illegal in many jurisdictions (US BOTS Act) and out of scope entirely.
  • Pin geography deliberately with -country-XX so every price row is tied to a known market and currency; never let an undirected exit decide which localized inventory you record.
  • Prefer the public JSON feeds over rendered HTML on StubHub and SeatGeek - they are more stable, cheaper to fetch, and give you structured price/fee fields - but health-check the endpoint paths since they rotate.
  • Rotate per request by default and reserve sticky sessions for genuine multi-request bursts (page-load-then-JSON); holding a sticky exit longer than needed just concentrates load on one IP.
  • Validate freshness and shape before writing - assert non-empty listings, sane price bands, and matching currency so soft-blocks and stale caches never contaminate your dataset.
  • Tier polling cadence by time-to-event and add jittered backoff; spend request velocity where prices actually move and back off hard on any challenge signal.

Common mistakes that burn proxy budget

  • Treating a 200 OK as valid data - storing empty or truncated listing arrays as if the event genuinely sold out, silently corrupting your price history.
  • Retrying a Ticketmaster Queue-it/challenge page immediately on a fresh IP instead of backing off - the door is closed for everyone, and you just burn clean residential exits against it.
  • Ignoring exit geography so US, UK, and CA inventory get mixed into one price series with mismatched currencies that look numerically comparable but are not.
  • Scraping rendered HTML on StubHub/SeatGeek instead of their JSON feeds, then breaking constantly on layout changes that never touched the underlying data.
  • Collapsing base price, fees, and all-in total into a single 'price' column, making primary face value and secondary resale asks falsely comparable.
  • Polling on a fixed, un-jittered interval - a perfectly periodic request cadence is itself a bot fingerprint that trips velocity-based detection even when each request looks human.

Common questions about this scraper problem

Is monitoring public ticket prices legal, and does it respect these sites' Terms of Service?
Collecting public, non-personal listing prices and availability for comparison or research is generally treated very differently from automated purchasing, but the specifics depend on your jurisdiction and each site's Terms of Service. Keep volume reasonable, collect only public non-personal data, respect ToS, and consult counsel for your situation. Automated PURCHASING of tickets - scalping bots, queue bypass, purchase-limit evasion - is a separate matter and is illegal in many places (e.g. the US BOTS Act of 2016); it is explicitly out of scope for this guide.
Does this guide help with buying tickets or beating the queue?
No, and deliberately so. This is about reading public price and availability data for price intelligence and consumer comparison only. Nothing here automates carts, checkout, waiting-room/queue bypass, or purchase-limit evasion - those activities are illegal in many jurisdictions and are entirely out of scope.
Why is Ticketmaster so much harder to read than StubHub or SeatGeek?
Ticketmaster fronts its pages with Akamai/PerimeterX-style device fingerprinting and Queue-it waiting rooms tuned against years of scalping-bot abuse, so even read-only traffic hits hostile defenses. StubHub and SeatGeek are lighter - mostly Cloudflare plus per-IP rate limits and rotating JSON paths - so clean residential IPs and a coherent header/referer flow get you much further there.
Why do I need proxies at all for public price data?
Because these sites block on IP reputation and request velocity, not just on whether the data is public. Polling many events on a fresh cadence from one IP looks exactly like the abuse pattern they defend against, so you get rate-limited or challenged fast. Rotating residential exits spread each request across real consumer IPs and let you pin the correct market geography.
When should I use a sticky session versus per-request rotation?
Per-request rotation is the default and right for independent price checks - each poll gets a fresh exit. Use a sticky session (up to 30 minutes) only when a single logical operation spans multiple requests that must look coherent, like warming a StubHub event page and then fetching its listings JSON with the matching referer from the same IP.
How do I get comparable prices across the three marketplaces?
Pin the same exit country for a given event so currency and localized inventory line up, then normalize into a schema that keeps base price, estimated fees, and fee-inclusive total as separate fields. Ticketmaster face value, StubHub all-in resale, and SeatGeek deal-score pricing only become meaningfully comparable once fees and currency are explicit rather than blended into one number.
How often should I poll to catch price movements?
Tier it by time-to-event: roughly every 5 minutes in the final 24 hours, every 30 minutes in the last week, and hourly further out, since prices move fastest right before an event. Add random jitter to every interval - a perfectly periodic poll is itself a detectable fingerprint.
I'm getting 200 OK but empty listings - is the event sold out?
Almost never. An empty or truncated listing array with a 200 status is usually a soft block or a rotated/expired JSON endpoint path, not a real sell-out. Validate for non-empty inventory and known-live events before trusting it, and treat a sudden row-count drop as an endpoint-drift or throttle signal to investigate.
Which Aethyn tier and port should I use?
Use the Elite tier on port 5499. Elite gives you the cleanest residential and mobile pools with per-request rotation, which is what keeps you under the reputation and velocity thresholds on the hardest surface here (Ticketmaster). The full endpoint is proxy.aethyn.io and rotation, country, city, and session are all encoded in the username.
Do I need a headless browser, or is a plain HTTP client enough?
For StubHub and SeatGeek, a plain HTTP client like requests hitting their public JSON feeds with a coherent header and referer flow is usually enough and far cheaper than rendering. Ticketmaster's Akamai/PerimeterX layer sometimes requires a real browser (Playwright) to clear fingerprint and challenge checks on hot events. Start with requests and escalate to a headless browser only for the specific endpoints that actually challenge you.
Share this article

Solve this with Aethyn residential proxies

Route your first request through rotating residential IPs in minutes, with transparent per-byte pricing.

All solutions