Get sales data (batch)
/v1/events/sales/batchReturns 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.
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=allThe 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 |
Pass Authorization: Bearer <api_key>. Preferred for new integrations.
In: header
Query Parameters
Marketplace filter, sent on the URL rather than in the body. 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"
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/v1/events/sales/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_ids_sh": [ 105294241, 105288127 ], "event_ids": [ 225220 ] }'{ "results": { "property1": [ { "timestamp": 1789489812, "quantity": 2, "price": 145, "zone": "Lower Level", "section": "112", "row": "12", "listing_id": 4871203955, "source": "sh", "all_in_price": null } ], "property2": [ { "timestamp": 1789489812, "quantity": 2, "price": 145, "zone": "Lower Level", "section": "112", "row": "12", "listing_id": 4871203955, "source": "sh", "all_in_price": null } ] }, "errors": { "property1": "not_found", "property2": "not_found" }, "sources": { "property1": [ { "source": "sh", "collecting_since": "2026-01-15", "tracked_for_event": true, "status": "ok" } ], "property2": [ { "source": "sh", "collecting_since": "2026-01-15", "tracked_for_event": true, "status": "ok" } ] }}Get sales data GET
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](https://docs.seatdata.io/docs/api/migrating-to-v1/). ## 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`: ```python 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`: ```bash tab="cURL" 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"} done wc -l sales.jsonl ``` ```python tab="Python" import requests base = "https://seatdata.io/api/v1/events/12345/sales" headers = {"Authorization": "Bearer YOUR_API_KEY"} rows = [] params = {"limit": 200} while True: response = requests.get(base, params=params, headers=headers) response.raise_for_status() page = response.json() rows.extend(page["data"]) if page["next_cursor"] is None: break params = {"starting_after": page["next_cursor"]} print(len(rows), "sales") ``` ```js tab="JavaScript" const base = "https://seatdata.io/api/v1/events/12345/sales"; const headers = { Authorization: "Bearer YOUR_API_KEY" }; const rows = []; let url = `${base}?limit=200`; while (url) { const response = await fetch(url, { headers }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const page = await response.json(); rows.push(...page.data); url = page.next_cursor ? `${base}?starting_after=${encodeURIComponent(page.next_cursor)}` : null; } console.log(rows.length, "sales"); ``` ```php tab="PHP" <?php $base = "https://seatdata.io/api/v1/events/12345/sales"; $headers = ["Authorization: Bearer YOUR_API_KEY"]; $rows = []; $url = $base . "?limit=200"; while ($url !== null) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, ]); $response = curl_exec($ch); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); $page = json_decode($response, true); $rows = array_merge($rows, $page["data"]); $url = $page["next_cursor"] !== null ? $base . "?starting_after=" . urlencode($page["next_cursor"]) : null; } echo count($rows) . " sales\n"; ``` ```go tab="Go" package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" ) type salesPage struct { Data []json.RawMessage `json:"data"` HasMore bool `json:"has_more"` NextCursor *string `json:"next_cursor"` } func main() { base := "https://seatdata.io/api/v1/events/12345/sales" var rows []json.RawMessage pageURL := base + "?limit=200" for pageURL != "" { req, err := http.NewRequest("GET", pageURL, nil) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer YOUR_API_KEY") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { log.Fatalf("HTTP %d", res.StatusCode) } var page salesPage if err := json.NewDecoder(res.Body).Decode(&page); err != nil { log.Fatal(err) } res.Body.Close() rows = append(rows, page.Data...) if page.NextCursor == nil { pageURL = "" continue } pageURL = base + "?starting_after=" + url.QueryEscape(*page.NextCursor) } fmt.Println(len(rows), "sales") } ``` ### 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: ```json { "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"} ] } ```
Get sales data GET
All responses are gzip encoded. You must pass either a **SeatData Event ID** (from the `/v0.3.1/events/search` or `/v1/events/search` endpoints) or a **Marketplace Event ID** (obtained separately). This endpoint returns data from a single marketplace. Multi-source sales data is available on `GET /v1/events/{event_id}/sales`.