Guides

Compare Ticket Prices Across Marketplaces with One API

The same seats can cost more on one marketplace than another. Find the cheapest all-in tickets for an event across Ticketmaster, StubHub, SeatGeek, Vivid Seats and more with one script.

Last updated

The same two seats for the Eagles at Sphere on December 11, 2026 (section 110, row 36, seats 4, 5) were listed on 4 marketplaces at once. Per ticket, all-in, as captured on October 18, 2026:

  • $385 on TickPick
  • $397 on SeatGeek
  • $403 on Vivid Seats
  • $406 on Gametime

Buy the pair on Gametime and you pay $42 more for the same seats.

Finding that by hand means opening 7 sites. The script below checks all 7 in one run, keeps only the listings that can actually sell you a pair, and ranks them by what you'd pay.

The cheapest pair, per marketplace

MarketplaceCheapest pair, per ticketSection · rowFeeListings that sell 2
TickPick$385.00110 · 36Included609 of 729
Ticketmaster$396.33FLR1 · 22Itemized43 of 52
SeatGeek$397.00110 · 36Itemized795 of 933
Vivid Seats$403.00110 · 36Itemized547 of 652
Gametime$406.00110 · 36Itemized113 of 307
StubHub$410.58110 · 36Included282 of 345
GoTickets$423.00110 · 36Itemized170 of 223
Live captures from October 18, 2026, all 7 marketplaces within two minutes. Prices are all-in per ticket in USD, before sales tax, and change all the time.

Run it

Get an API key, save compare.py below, install requests, and run it. The command uses the first upcoming show the search finds, or the one you click:

matched eventsSearching…

Searching the catalog…

Compare a pair · The Eagles, Dec 11 (example)
export TICKETS_DEV_KEY=your_api_key
python compare.py "https://www.stubhub.com/the-eagles-las-vegas-tickets-12-11-2026/event/161551904" --quantity 2 --top 8
Output for The Eagles at Sphere, captured October 18, 2026
$ python compare.py "eagles sphere" --quantity 2 --top 8
The Eagles | Sphere at The Venetian Resort | 2026-12-11T20:30:00-08:00

Marketplace   Captured (UTC)       Matching listings
ticketmaster  2026-10-18T19:42:24  43
vividseats    2026-10-18T19:42:33  547
stubhub       2026-10-18T19:42:46  282
seatgeek      2026-10-18T19:42:57  795
tickpick      2026-10-18T19:43:12  609
gametime      2026-10-18T19:43:21  113
gotickets     2026-10-18T19:43:35  170

Cheapest 2 tickets, all-in, USD
Marketplace   Section   Row    Per ticket       For 2  Fee itemized
tickpick      110       36         385.00      770.00  no, included
tickpick      101       27         394.00      788.00  no, included
ticketmaster  FLR1      22         396.33      792.66  yes
ticketmaster  FLR1      17         396.33      792.66  yes
ticketmaster  FLR1      18         396.33      792.66  yes
ticketmaster  FLR1      19         396.33      792.66  yes
ticketmaster  FLR1      20         396.33      792.66  yes
ticketmaster  FLR5      22         396.33      792.66  yes

Pass any event name or a marketplace event URL. Add --section 110 or --row 12 to narrow it down, and --top for more rows.

compare.py
"""Compare one event's live listings across ticket marketplaces.

    python compare.py "<event name or URL>" --quantity 2
    python compare.py "<event name or URL>" --quantity 2 --section 110

Set TICKETS_DEV_KEY to your API key. Accessible seating (wheelchair and
companion seats) is left out unless you pass --accessible.
"""
import argparse
import os
import re
import time
from concurrent.futures import ThreadPoolExecutor

import requests

API = "https://api.tickets.dev/v1"
HEADERS = {"x-api-key": os.environ["TICKETS_DEV_KEY"]}

# Marketplaces name some sections differently: the same floor section can be
# "Floor 5" on one and "5" on another. Map names you have checked to one key,
# per marketplace. Anything unmapped is compared by its own name, lowercased.
SECTION_ALIASES = {
    # ("vividseats", "floor 5"): "5",
}


def find_event(query):
    """Step 1: one event, with its URL on every marketplace that lists it."""
    response = requests.get(
        f"{API}/events", params={"query": query}, headers=HEADERS, timeout=30
    )
    if not response.ok:
        raise SystemExit(f"Event lookup failed: {error_code(response)}")
    events = response.json()["events"]
    if not events:
        raise SystemExit(f"No upcoming event matches {query!r}")
    if len(events) > 1:
        print(f"{len(events)} events match, using the first. "
              "Pass a marketplace event URL to pick one exactly.")
    return events[0]


def error_code(response):
    try:
        return response.json()["error"]["code"]
    except (ValueError, KeyError, TypeError):
        return f"HTTP {response.status_code}"


def capture(source):
    """Step 2: one capture is one event on one marketplace. Retries what the
    API marks retryable, such as 429 rate_limited when capacity is full, and
    never raises, so one marketplace failing doesn't take the others down."""
    marketplace = source["marketplace"]
    wait = 0
    for attempt in range(3):
        time.sleep(wait)
        try:
            response = requests.get(
                f"{API}/capture", params={"url": source["url"]}, headers=HEADERS,
                timeout=120,
            )
        except requests.RequestException as error:
            failure, wait = type(error).__name__, 10
            continue
        if response.ok:
            return {"marketplace": marketplace, "snapshot": response.json()}
        failure = error_code(response)
        try:
            retryable = response.json()["error"]["retryable"]
        except (ValueError, KeyError, TypeError):
            retryable = False
        if not retryable:
            break
        retry_after = response.headers.get("Retry-After", "")
        wait = int(retry_after) if retry_after.isdigit() else 10
    return {"marketplace": marketplace, "error": failure}


def sellable(listing):
    """The quantities a buyer can actually buy. A listing of 3 that sells
    as "1,3" can't sell you 2. With no split rules published, assume only
    the whole listing."""
    quantities = listing["sellableQuantities"].replace(" ", "").split(",")
    return {int(q) for q in quantities if q.isdigit()} or {listing["quantity"]}


ACCESSIBLE = re.compile(r"accessib|wheelchair|companion", re.IGNORECASE)


def accessible(listing):
    """Wheelchair and companion seats: row WC, or said so in the listing."""
    text = f"{listing.get('ticketType') or ''} {listing.get('listingNotes') or ''}"
    return listing["row"].strip().upper() == "WC" or bool(ACCESSIBLE.search(text))


def normalize(marketplace, snapshot):
    """Step 3: one row per listing, with the fields the comparison needs."""
    rows = []
    for listing in snapshot["listings"]:
        section = listing["section"].strip().casefold()
        total = listing["totalPrice"]
        rows.append({
            "marketplace": marketplace,
            "section": listing["section"],
            "section_key": SECTION_ALIASES.get((marketplace, section), section),
            "row": listing["row"],
            "quantities": sellable(listing),
            # All-in per ticket. A missing total stays missing: falling back
            # to ticketPrice would rank a pre-fee price against all-in ones.
            "total": total if isinstance(total, (int, float)) and total > 0 else None,
            "fee_itemized": listing["fee"] > 0,
            "accessible": accessible(listing),
            "currency": snapshot["currency"],
        })
    return rows


def matches(row, quantity, section, row_label, include_accessible=False):
    """Step 4: listings that can sell this many tickets, where you want them."""
    if row["accessible"] and not include_accessible:
        return False
    if quantity not in row["quantities"]:
        return False
    if section and row["section_key"] != section.strip().casefold():
        return False
    if row_label and row["row"].strip().casefold() != row_label.strip().casefold():
        return False
    return True


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("event", help="event name, or any marketplace event URL")
    parser.add_argument("--quantity", type=int, default=2)
    parser.add_argument("--section")
    parser.add_argument("--row")
    parser.add_argument("--top", type=int, default=10)
    parser.add_argument("--accessible", action="store_true",
                        help="include wheelchair and companion seats")
    args = parser.parse_args()

    event = find_event(args.event)
    print(f"{event['name']} | {event['venue']['name']} | {event['eventDateLocal']}")

    # Plans cap how many captures run at once, so keep the pool small.
    with ThreadPoolExecutor(max_workers=4) as pool:
        results = list(pool.map(capture, event["sources"]))

    comparable = []
    print(f"\n{'Marketplace':<13} {'Captured (UTC)':<20} Matching listings")
    for result in results:
        marketplace = result["marketplace"]
        if "error" in result:
            print(f"{marketplace:<13} {'unavailable':<20} {result['error']}")
            continue
        snapshot = result["snapshot"]
        rows = [r for r in normalize(marketplace, snapshot)
                if matches(r, args.quantity, args.section, args.row, args.accessible)]
        priced = [r for r in rows if r["total"] is not None]
        comparable += priced
        detail = f"{len(rows)}"
        if len(priced) < len(rows):
            detail += f" ({len(rows) - len(priced)} without an all-in price)"
        if not snapshot["listings"] and snapshot["note"]:
            detail += f" | {snapshot['note'][:60]}"
        print(f"{marketplace:<13} {snapshot['capturedAt'][:19]:<20} {detail}")

    if not comparable:
        print("\nNo listing on any marketplace matches those filters.")

    # Step 5: rank by all-in price, one currency at a time.
    for currency in sorted({r["currency"] for r in comparable}):
        best = sorted((r for r in comparable if r["currency"] == currency),
                      key=lambda r: r["total"])[:args.top]
        print(f"\nCheapest {args.quantity} tickets, all-in, {currency}")
        print(f"{'Marketplace':<13} {'Section':<9} {'Row':<5} "
              f"{'Per ticket':>11} {f'For {args.quantity}':>11}  Fee itemized")
        for r in best:
            print(f"{r['marketplace']:<13} {r['section']:<9} {r['row']:<5} "
                  f"{r['total']:>11,.2f} {r['total'] * args.quantity:>11,.2f}  "
                  f"{'yes' if r['fee_itemized'] else 'no, included'}")


if __name__ == "__main__":
    main()

What it handles for you

  • It counts only listings you can buy. Sellers set split rules, so a listing's quantity isn't always an amount you can order. On StubHub, the 3 tickets in section 404, row 18 sell as 1 or 3, so they can't fill an order for 2. Of 3,241 listings across the 7 marketplaces, 2,559 can sell a pair.
  • It ranks on totalPrice, the all-in price per ticket. Ticketmaster, Vivid Seats, SeatGeek, Gametime and GoTickets itemize a fee, while StubHub and TickPick show one all-in figure with the fee already inside. A listing without an all-in price is counted and left out of the ranking, because a price before fees always looks cheaper than it is.
  • Section names don't match across marketplaces. The same floor section is "FLR1" on Ticketmaster, "Floor 1" on Vivid Seats, "floor 1" on SeatGeek and "1" on Gametime. Add the ones you've checked to SECTION_ALIASES.
  • It leaves out wheelchair and companion seats, which most buyers can't use. Pass --accessible to include them.
  • If a marketplace fails, it shows as unavailable and the rest still compare. Each marketplace shows when it was captured, and prices are ranked one currency at a time.

How the capture works

The script makes two kinds of call. /v1/events finds the event and its URL on every marketplace that lists it, and is never billed. Then one capture per marketplace returns every listing on that event page, live, in the same listing schema. For matching events, capturing and normalizing listings step by step, see how to track ticket prices across marketplaces.

What it costs

One comparison is one capture per marketplace: 7 here, so $0.35 on the Starter plan ($0.050 a capture), down to $0.035 on High Volume ($0.005). Event lookups and failed captures are never billed. If you show comparisons on a website, keep the latest capture per marketplace and refresh it on a schedule. Capturing on every page view multiplies the bill by your traffic. Plans are on the pricing page.

Going further

  • For a web page, return the ranked rows as JSON from your server and render them, so the key never reaches the browser.
  • To show seat maps, capture with includeVenueMaps=true. Each snapshot then has an SVG section map whose shapes carry that marketplace's section names. Fetch the SVG on your server and send it with the rows; see the seat map API.
  • For price history, run it on a schedule and keep every capture, as in tracking ticket prices.
  • To hear when a pair drops under your price, set up ticket price and inventory alerts.

Try it on a live event

Paste any supported event URL and you get back every listing with its all-in price, in the same schema for every marketplace. Failed captures are never billed.

TicketmasterStubHubSeatGeekTickPickVivid Seats+10
Get API keyOpen the playground