SeatDataDocs

Migrating Sales Data to v1

How to move from the v0.3 sales endpoints to their v1 equivalents

The sales data endpoints now have v1 equivalents:

v0.3v1
GET /api/v0.3/salesdata/getGET /api/v1/events/{event_id}/sales
POST /api/v0.3/salesdata/batchPOST /api/v1/events/sales/batch

Both versions are live. The v0.3 endpoints are not deprecated and remain fully supported — v1 is the modern contract, not a forced cutover. v1 returns the same sales rows plus an integer listing_id on every row, and adds cursor pagination, a JSON response envelope, and structured JSON errors.

Billing is identical

Migrating to v1 does not change what a pull costs. A first page returning at least one sale is one billable pull; an event with no sales is free; continuation (cursor) pages are free. Fetching an event's full history through v1 pagination costs the same single pull as one v0.3 call. Batch billing is unchanged: one pull per event in the batch that returns data.

Authentication — nothing to migrate

Same 64-character key, same headers. Authorization: Bearer <key> is preferred; the legacy api-key: <key> header also works on every endpoint.

Single-event sales: GET /v1/events/{event_id}/sales

What changed from GET /v0.3/salesdata/get:

v0.3v1
RouteGET /api/v0.3/salesdata/getGET /api/v1/events/{event_id}/sales
Event selectorquery ?event_id= (SeatData ID) or ?event_id_sh= (Marketplace ID)ID in the path; SeatData ID by default. For a Marketplace ID, add ?id_type=marketplace
Response bodybare JSON array of rowsJSON object: { "event_id", "data": [rows], "has_more", "next_cursor", "total_count" }
Encodinggzip (Content-Encoding: gzip)plain application/json, not gzipped
Paginationnone — returns all rowscursor pages: ?limit= (default 100, max 200), ?starting_after=<next_cursor>
Row fieldstimestamp, quantity, price, zone, section, rowsame plus listing_id (integer)
Row orderingtimestamp descendingtimestamp descending, then id descending (stable tiebreak for paging)
Errorsplain-text body + statusJSON error envelope (see below)

The three changes most likely to break a naive port

  1. Pagination can silently truncate. v0.3 returned every row in one array. v1 returns at most limit rows (default 100) with has_more: true and a next_cursor. A client that reads only data and stops will miss every row past the first page. To get everything, keep calling with starting_after=<next_cursor> until next_cursor is null (has_more false). Raise limit to 200 to cut round trips.
  2. The response is an object, not an array. Read rows from data, not the top level.
  3. No gzip. v1 responses are plain JSON — drop any hard-coded gzip decompression.

Requests

GET /api/v1/events/12345/sales?limit=200 HTTP/1.1
Authorization: Bearer <your-64-character-key>

Marketplace Event ID instead of SeatData Event ID:

GET /api/v1/events/98765/sales?id_type=marketplace HTTP/1.1
Authorization: Bearer <your-64-character-key>

Next page:

GET /api/v1/events/12345/sales?starting_after=<next_cursor> HTTP/1.1
Authorization: Bearer <your-64-character-key>

Success shape

{
  "event_id": 12345,
  "data": [
    { "timestamp": 1722900000, "quantity": 2, "price": 145.0,
      "zone": "...", "section": "...", "row": "...", "listing_id": 987654321 }
  ],
  "has_more": true,
  "next_cursor": "opaque-token-string",
  "total_count": 4213
}
  • total_count appears on the first page only (omitted on continuation pages).
  • next_cursor is opaque — pass it back verbatim, don't parse it. It's scoped to that one event; a cursor from another event or endpoint returns 400 invalid_cursor. Cursors expire after 1 hour (same as the stats endpoint).
  • event_id echoes the literal value from the path.

Each row is a validated sale — a real transaction that was observed. quantity is inferred and may be 0; price reflects the listing price at the time of observation.

Batch sales: POST /v1/events/sales/batch

What changed from POST /v0.3/salesdata/batch:

v0.3v1
RoutePOST /api/v0.3/salesdata/batchPOST /api/v1/events/sales/batch
Body{ "event_ids": [...], "event_ids_sh": [...] }same
Max events100 (combined, deduped)same
Response{ "results": {"<sent id>": [rows]}, "errors": {"<sent id>": "not_found" | "payment_required"} }same shape
Encodinggzipplain application/json
Row fieldsv0.3 rowsame plus listing_id
Malformed bodyplain-text 400JSON error envelope, 400 invalid_param
  • results/errors are keyed by the exact identifier string you sent (a SeatData ID and a Marketplace ID that resolve to the same event are echoed under both keys).
  • Batch has no pagination and no id_type parameter — it returns all rows per event, driven by the two-list body. Only the single-event GET paginates.
  • Billing is per-event: each event that returns rows is charged once; empty events are free. If the balance runs out mid-batch, already-affordable events are still served and the rest come back as "payment_required" in errors; if nothing is affordable, the endpoint returns 402. Identical to v0.3.

Error envelope (both v1 endpoints)

v0.3 returned plain-text errors and collapsed most failures into generic 400/401 responses. v1 returns a structured envelope so clients can branch on the specific failure:

{ "error": { "type": "...", "code": "...", "message": "...", "param": "..." } }

param appears only for input-validation errors. Codes these endpoints emit:

StatustypecodeWhen
400invalid_requestinvalid_parambad event_id, bad id_type, malformed batch body
400invalid_requestinvalid_cursorexpired/unknown/wrong-scope starting_after
401authentication_errormissing_api_key / invalid_api_keyno/malformed key, or key not found
402(balance envelope)insufficient_balancepay-as-you-go account out of funds
403subscription_requiredsubscription_requiredkey valid but no active API access
404not_foundevent_not_foundunknown event (including id_type=marketplace with an unknown ID)
429rate_limit_errorrate_limitedover the limit; response carries a Retry-After header

For current rate limits, treat GET /v1/account and the rate-limit response headers as authoritative — see the Rate Limits guide.

On this page