SeatDataDocs

Get sales data

GET/v1/events/{event_id}/sales

Returns sales records for a single event - each row is a real sale transaction. Rows are ordered by timestamp descending, then by id descending within a source (a stable tiebreak for paging). Under source=all, two sales sharing a timestamp are ordered sh first, then by that source's id tiebreak.

The v1 equivalent of GET /v0.3/salesdata/get. Same data, same billing - plus cursor pagination, a JSON response envelope, a listing_id on every row, and structured JSON errors. Responses are application/json, and v1 never forces gzip the way v0.3 does - any compression is ordinary content negotiation your HTTP client handles for you. See the migration guide.

Sources

Sales are observed on more than one marketplace. The optional source query parameter selects which rows you receive.

sourceReturns
omitted, or shThe default. Exactly the rows and charges callers received before multi-source support.
vsOnly sales observed on the second marketplace.
allBoth, merged newest-first.

Every row carries a source field naming the marketplace it was observed on. The two sources do not carry the same keys, so read source to tell which shape a row has:

sh rowvs row
listing_idintegerstring
zoneas reportedalways "" - use norm_zone
all_in_pricealways nullthe fee-inclusive price, when shown
norm_zone, norm_sectionabsentpresent

Sales whose seat details could not be determined are not returned by these endpoints, and are not counted in total_count. On vs rows, sales are observed from listing changes between pulls, so the per-event count is a lower bound.

Joining the two sources

norm_zone and norm_section exist so a vs row can be matched to the sh rows for the same seats. They hold the zone and section names used for those seats in sh sales for that event or venue, derived by matching rather than reported by the marketplace, and are "" when no match is found. sh rows carry neither field - their names are the naming space being translated into, so there is nothing to translate.

To join, compare a vs row's norm_section against a sh row's section, and its norm_zone against zone:

primary   = [r for r in rows if r["source"] == "sh"]secondary = [r for r in rows if r["source"] == "vs"]for sale in secondary:    same_seats = [        r for r in primary        if r["section"] == sale["norm_section"]        and r["zone"] == sale["norm_zone"]    ]

Compare listing_id as a string if you compare it at all - it is an integer on sh rows and a string on vs rows.

A source value outside the three above returns 400 invalid_param with param set to source. That response is free - it is rejected before any billing or data read.

Quantity and price

sh sales often reach us unenriched, without a quantity or price. For those we derive both from the listing data we hold for that listing; a quantity of 0 means it could not be reliably determined. vs sales carry both as reported.

Billing rules

ScenarioCharged a pull?
First page returns at least one saleYes - 1 pull
Follow-up paginated page (cursor present)No (continuation)
Event has no salesNo (empty result, free)

Fetching an event's full history through pagination costs the same single pull as one GET /v0.3/salesdata/get call. The source value does not change what a page costs.

Pagination

Cursor-based. The first page response includes total_count; subsequent paginated pages omit it. next_cursor is opaque - pass it back verbatim via starting_after; don't parse it. Cursors are scoped to the event and the source they were issued under, and expire after 1 hour. Replaying a cursor with a different source returns 400 invalid_cursor. Use one source value for every page of a walk.

Example: fetching every page

Each response returns at most limit rows. To fetch an event's full sales history, keep requesting with starting_after=<next_cursor> until next_cursor is null:

API_KEY="YOUR_API_KEY"BASE="https://seatdata.io/api/v1/events/12345/sales"URL="$BASE?limit=200"while [ -n "$URL" ]; do  PAGE=$(curl -s "$URL" -H "Authorization: Bearer $API_KEY")  echo "$PAGE" | jq -c '.data[]' >> sales.jsonl  CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')  URL=${CURSOR:+"$BASE?starting_after=$CURSOR"}donewc -l sales.jsonl

Example: multi-source response

GET /api/v1/events/12345/sales?source=all&limit=2 returns one row from each marketplace, merged newest-first. Note the differing key sets - the second row carries the normalized fields, the first does not:

{  "event_id": 12345,  "data": [    {"timestamp": 1789489812, "quantity": 2, "price": 145.0, "zone": "Lower Level",     "section": "112", "row": "12", "listing_id": 4871203955, "source": "sh",     "all_in_price": null},    {"timestamp": 1789489140, "quantity": 2, "price": 139.0, "zone": "",     "section": "Lower Level 112", "row": "14", "listing_id": "8815520431",     "source": "vs", "all_in_price": 171.5,     "norm_zone": "Lower Level", "norm_section": "112"}  ],  "has_more": true,  "next_cursor": "opaque-token-string",  "total_count": 42,  "sources": [    {"source": "sh", "collecting_since": "2026-01-15", "tracked_for_event": true, "status": "ok"},    {"source": "vs", "collecting_since": "2026-08-30", "tracked_for_event": true, "status": "ok"}  ]}

Authorization

AuthorizationBearer <token>

Pass Authorization: Bearer <api_key>. Preferred for new integrations.

In: header

Path Parameters

event_id*integer

SeatData Event ID (from /v1/events/search) by default. To pass a Marketplace Event ID instead, add ?id_type=marketplace.

Query Parameters

id_type?"marketplace"

Set to marketplace to interpret the path id as a Marketplace Event ID. Omit to interpret it as a SeatData Event ID.

Value in

  • "marketplace"
source?string

Marketplace filter. sh (the default) returns exactly the rows and charges callers received before multi-source support. vs returns only sales observed on the second marketplace. all returns both, merged newest-first. Any other value returns 400 invalid_param, free of charge.

Default"sh"

Value in

  • "sh"
  • "vs"
  • "all"
limit?integer

Max sales rows per page (default 100, max 200)

Rangevalue <= 200
Default100
starting_after?string

Cursor from a previous response's next_cursor

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v1/events/225220/sales" \  -H "Authorization: Bearer YOUR_API_KEY"
{  "event_id": 225220,  "total_count": 4213,  "data": [    {      "timestamp": 1789489812,      "quantity": 2,      "price": 145,      "zone": "Lower Level",      "section": "112",      "row": "12",      "listing_id": 4871203955,      "source": "sh",      "all_in_price": null    }  ],  "has_more": true,  "next_cursor": "string",  "sources": [    {      "source": "sh",      "collecting_since": "2026-01-15",      "tracked_for_event": true,      "status": "ok"    }  ]}

Get event stats GET

Returns the historical event_stats time series for a single event. Each snapshot row includes event-level aggregates (avg/median price, get-in price, listing fill rate) AND inline zone-level breakdowns for every zone in that snapshot. ## Billing rules | Scenario | Charged a pull? | |---|---| | First request for an event (no cursor, snapshots exist) | Yes - 1 pull | | Follow-up paginated page (cursor present) | No (continuation) | | Repeat first-page request, no new snapshot since last paid pull | No (freshness rule) | | Repeat first-page request, new snapshot logged in between | Yes - 1 pull | | Event has no event_stats rows yet | No (empty result, free) | The freshness rule means polling for "is there fresh data?" only costs when there's actually new data to consume. ## Pagination Cursor-based. The first page response includes `total_count` and `available_zones`; subsequent paginated pages omit them. Cursors expire after 1 hour.

Get sales data (batch) POST

Returns sales records for up to 100 events per request, combined across both id lists after deduplication. Each row is a real sale transaction. Rows have the same shapes as on `GET /v1/events/{event_id}/sales`, including the per-source key differences described there. The v1 equivalent of `POST /v0.3/salesdata/batch` - same request body, same response shape, same billing. Differences: responses are `application/json` with no forced gzip, rows include `listing_id`, and a malformed body returns a JSON error envelope instead of plain text. See the [migration guide](https://docs.seatdata.io/docs/api/migrating-to-v1/). There is no pagination and no `id_type` parameter here - the two id lists in the body drive the request, and all rows are returned for each event. ## Sources The optional `source` parameter behaves exactly as it does on `GET /v1/events/{event_id}/sales`: `sh` (the default), `vs`, or `all`. Send it on the URL - it is a query-string parameter, not a body field: ``` POST /api/v1/events/sales/batch?source=all ``` The response carries a top-level `sources` object, keyed by the same client-sent event ids as `results`. Each value lists both marketplaces, `sh` first, regardless of the `source` filter. `results` and `errors` are unchanged in shape. An invalid `source` returns `400` `invalid_param` with `param` set to `source`, free of charge. `results` and `errors` are keyed by the exact identifier string you sent: Marketplace Event IDs appear under the marketplace id, SeatData Event IDs under the SeatData id (a SeatData ID and a Marketplace ID that resolve to the same event are echoed under both keys). ## Quantity and price `sh` sales often reach us unenriched, without a quantity or price. For those we derive both from the listing data we hold for that listing; a `quantity` of `0` means it could not be reliably determined. `vs` sales carry both as reported. ## Billing rules | Scenario | Charged a pull? | |---|---| | Event in the batch returns at least one sale | Yes - 1 pull per such event | | Event in the batch has no sales | No (free) | | Balance runs out mid-batch | Events already affordable are served and charged; the rest come back as `payment_required` in `errors` | | No event in the batch is affordable | No data - the request returns 402 |