Get sales data
/v1/events/{event_id}/salesReturns 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.
source | Returns |
|---|---|
omitted, or sh | The default. Exactly the rows and charges callers received before multi-source support. |
vs | Only sales observed on the second marketplace. |
all | Both, 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 row | vs row | |
|---|---|---|
listing_id | integer | string |
zone | as reported | always "" - use norm_zone |
all_in_price | always null | the fee-inclusive price, when shown |
norm_zone, norm_section | absent | present |
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
| Scenario | Charged a pull? |
|---|---|
| First page returns at least one sale | Yes - 1 pull |
| Follow-up paginated page (cursor present) | No (continuation) |
| Event has no sales | No (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.jsonlExample: 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"} ]}Pass Authorization: Bearer <api_key>. Preferred for new integrations.
In: header
Path Parameters
SeatData Event ID (from /v1/events/search) by default. To pass a Marketplace Event ID instead, add ?id_type=marketplace.
Query Parameters
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"
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.
"sh"Value in
- "sh"
- "vs"
- "all"
Max sales rows per page (default 100, max 200)
value <= 200100Cursor 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 |