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.3 | v1 |
|---|---|
GET /api/v0.3/salesdata/get | GET /api/v1/events/{event_id}/sales |
POST /api/v0.3/salesdata/batch | POST /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.3 | v1 | |
|---|---|---|
| Route | GET /api/v0.3/salesdata/get | GET /api/v1/events/{event_id}/sales |
| Event selector | query ?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 body | bare JSON array of rows | JSON object: { "event_id", "data": [rows], "has_more", "next_cursor", "total_count" } |
| Encoding | gzip (Content-Encoding: gzip) | plain application/json, not gzipped |
| Pagination | none — returns all rows | cursor pages: ?limit= (default 100, max 200), ?starting_after=<next_cursor> |
| Row fields | timestamp, quantity, price, zone, section, row | same plus listing_id (integer) |
| Row ordering | timestamp descending | timestamp descending, then id descending (stable tiebreak for paging) |
| Errors | plain-text body + status | JSON error envelope (see below) |
The three changes most likely to break a naive port
- Pagination can silently truncate. v0.3 returned every row in one array. v1 returns at most
limitrows (default 100) withhas_more: trueand anext_cursor. A client that reads onlydataand stops will miss every row past the first page. To get everything, keep calling withstarting_after=<next_cursor>untilnext_cursorisnull(has_morefalse). Raiselimitto 200 to cut round trips. - The response is an object, not an array. Read rows from
data, not the top level. - 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_countappears on the first page only (omitted on continuation pages).next_cursoris opaque — pass it back verbatim, don't parse it. It's scoped to that one event; a cursor from another event or endpoint returns400 invalid_cursor. Cursors expire after 1 hour (same as the stats endpoint).event_idechoes 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.3 | v1 | |
|---|---|---|
| Route | POST /api/v0.3/salesdata/batch | POST /api/v1/events/sales/batch |
| Body | { "event_ids": [...], "event_ids_sh": [...] } | same |
| Max events | 100 (combined, deduped) | same |
| Response | { "results": {"<sent id>": [rows]}, "errors": {"<sent id>": "not_found" | "payment_required"} } | same shape |
| Encoding | gzip | plain application/json |
| Row fields | v0.3 row | same plus listing_id |
| Malformed body | plain-text 400 | JSON error envelope, 400 invalid_param |
results/errorsare 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_typeparameter — 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"inerrors; 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:
| Status | type | code | When |
|---|---|---|---|
| 400 | invalid_request | invalid_param | bad event_id, bad id_type, malformed batch body |
| 400 | invalid_request | invalid_cursor | expired/unknown/wrong-scope starting_after |
| 401 | authentication_error | missing_api_key / invalid_api_key | no/malformed key, or key not found |
| 402 | (balance envelope) | insufficient_balance | pay-as-you-go account out of funds |
| 403 | subscription_required | subscription_required | key valid but no active API access |
| 404 | not_found | event_not_found | unknown event (including id_type=marketplace with an unknown ID) |
| 429 | rate_limit_error | rate_limited | over 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.