Seatpin Seller API Documentation
Version: v1
Base URL: https://seller.seatpin.com/api/v1
Portal URL: https://seller.seatpin.com
Last Updated: 2026-07-24
Table of Contents
- Overview & Quick Start
- Authentication
- Base URL & Environments
- Rate Limiting
- IP Whitelisting
- Response Format & Error Codes
- Endpoints
- Webhooks
- Sub-sellers
1. Overview & Quick Start
What is the Seller API?
The Seatpin Seller API is a REST API that gives approved high-volume ticket sellers programmatic access to the Seatpin marketplace. It allows you to manage your entire inventory lifecycle, track sales and fulfilment, and pull detailed performance reports — all without using the Seatpin seller web interface.
Use Cases
- Inventory automation — sync listings from your own inventory system into Seatpin in bulk, update prices in real time, publish/unpublish on demand
- Sales fulfilment — poll for new orders, retrieve buyer PII (proportional to ticket format requirements), upload e-tickets or tracking numbers
- Performance reporting — aggregate revenue, order counts, and per-listing metrics grouped by day/week/month/year
- Webhook-driven workflows — receive push notifications to your own server the moment a listing or order changes state
Onboarding Flow
- Contact Seatpin support to request Seller status for your account.
- Once approved, log in at https://seller.seatpin.com with your existing Seatpin credentials.
- Complete 2FA setup (TOTP app required — this is mandatory before API keys can be created).
- Navigate to Portal → API Keys → Create new key. Copy the token immediately — it is shown only once.
- Optionally add your server IPs under Portal → IP Whitelist to restrict which machines can call the API.
- Optionally configure webhook endpoints under Portal → Webhooks to receive push events.
- Start making API calls using
Authorization: Bearer {your_token}.
Postman & Bruno Collections
Ready-to-run API collections are available so you can start calling the API without hand-writing requests. Every endpoint in this document — including the Sandbox Order Simulation endpoints — is included, with example bodies and saved response examples.
⬇ Download collections (.zip) — contains both the Postman collection and the Bruno collection, plus ready-made Sandbox and Production environment files.
Postman
- Unzip and open Postman → Import → drop in
postman/Seatpin-PowerSeller-API.postman_collection.json. - Import the environment for the target stage:
postman/Seatpin-PowerSeller-API.postman_environment_sandbox.json(recommended to start) or..._production.json. - Select the imported environment (top-right), then set the
api_keyvariable to the token from Portal → API Keys. - The sandbox request Create Sandbox Order auto-saves the returned
order_idinto thesandbox_order_idvariable, so Pay / Cancel / Refund run against it with no manual copy-paste.
Bruno
- Unzip and open Bruno → Open Collection → select the
bruno/folder. - Pick the sandbox or production environment (top-right), then set the secret
api_keyvalue. - Same as Postman, 05 Sandbox → Create Order stores the new
order_idfor the follow-up lifecycle requests.
There is no separate OpenAPI/Swagger spec file — this document plus the Postman/Bruno collections are the source of truth for the request/response contract. The collections are kept in sync with the API on every release.
2. Authentication
All API endpoints (under /api/v1/...) require a Bearer token issued from the Seller Portal.
Token format: {id}|{random_secret} (Laravel Sanctum personal access token with the power-seller ability).
Creating a key: Portal → API Keys → Create new key. The plaintext token is displayed only once; store it securely (e.g., a secrets manager).
Revoking a key: Portal → API Keys → Revoke. After revocation, all API calls using that key return 401 Unauthorized.
Multiple keys: You may create multiple API keys (e.g., one per integration or environment). Each key can be revoked independently.
Security note: Treat your API key like a password. Never commit it to version control. Use environment variables or a secrets vault.
3. Base URL & Environments
| Environment | Base URL | Notes |
|---|---|---|
| Production | https://seller.seatpin.com/api/v1 |
Live marketplace data |
| Sandbox | https://sandbox-seller.seatpin.com/api/v1 |
Reduced-scope mirror of production for integration work |
The portal (2FA, API keys, webhooks, public docs) is served at the root of the same domain: https://seller.seatpin.com for production, https://sandbox-seller.seatpin.com for sandbox.
Sandbox vs Production
Sandbox is identical to production at the API contract level — same request and response shapes, same auth, same error envelope — but the dataset surface is capped so integrators can safely test against a small, realistic slice:
/events/searchreturns at most 10 events (the nearest upcoming ones), regardless of the requestedper_page.- The remaining endpoints behave normally but only see your sandbox account's listings, orders and webhooks.
- Sandbox additionally exposes the order simulation endpoints (
/sandbox/orders/..., see 7.5) so you can place a test order on your own listing and receive the realorder.*/payment.*webhooks end-to-end. These endpoints return404on production.
Build your integration against sandbox first; promote to production once the contract is wired up. Token, IP whitelist, and webhook configurations are independent between the two environments — set them up separately in each portal.
4. Rate Limiting
- Default limit: 180 requests per minute per API key
- Exceeded:
429 Too Many Requestswith aRetry-Afterheader (seconds until the window resets)
Best practice: implement exponential back-off in your client, and check the Retry-After header before retrying.
5. IP Whitelisting
IP whitelisting is optional. When no IPs are configured for your account, requests from any IP address are accepted.
Once you add one or more IPs:
- Only requests originating from those IPs are accepted
- Other IPs receive
403 Forbidden
Managing IPs: Portal → IP Whitelist → Add / Remove
IPv4 and IPv6 are both supported (addresses are normalised internally via inet_pton).
Reverse proxy environments: The API trusts X-Forwarded-For headers according to the server's TrustProxies configuration.
6. Response Format & Error Codes
Success Envelope
meta and links are only present on paginated endpoints. Non-paginated responses return data (and sometimes message).
Error Envelope
Every non-2xx response shares the same shape:
successis alwaysfalsefor error responses.messageis a short human-readable summary suitable to surface to your end users.errorsis an object keyed by field name for422 Unprocessable Entityvalidation failures. For all other status codes it is an empty object ({}).
Internal details (SQL errors, table names, file paths, stack traces) are stripped in production — clients see only the safe message. Enable APP_DEBUG=true in non-production environments if you need raw exception text while integrating.
HTTP Status Codes
| Code | When | Notes |
|---|---|---|
200 OK |
Successful GET or POST | |
201 Created |
Resource(s) created | Returned by bulk-add when all listings succeed |
207 Multi-Status |
Partial success | bulk-add with some failures; bulk-update-action with some failures |
400 Bad Request |
Malformed JSON body | |
401 Unauthorized |
Missing or invalid Bearer token | |
403 Forbidden |
Token valid but account not a Seller, or IP not whitelisted | |
404 Not Found |
Resource missing, or exists but owned by a different user | message is endpoint-specific (e.g. Order not found., Listing not found.) |
405 Method Not Allowed |
Wrong HTTP verb for this endpoint | |
422 Unprocessable Entity |
Validation failure — errors object present, or bulk-add/bulk-update-action where every item failed |
|
429 Too Many Requests |
Rate limit exceeded — Retry-After header present |
|
500 Internal Server Error |
Unexpected server error — contact support | Internal details are not exposed in production |
7. Endpoints
7.1 Events
GET /api/v1/events/search
Search for upcoming events available on the Seatpin marketplace. Only active, visible events with a future date are returned. Results are ordered by event date ascending.
Authentication: Bearer token required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
event_name |
string | no | Partial match on event name (translated) |
performer_name |
no | no | Partial match on performer/artist name |
venue_name |
string | no | Partial match on venue name |
start_date |
date (YYYY-MM-DD) |
no | Only events on or after this date |
end_date |
date (YYYY-MM-DD) |
no | Only events on or before this date. Must be >= start_date |
per_page |
integer | no | Results per page. Default: 20, max: 50 |
page |
integer | no | Page number for data. Default: 1 |
catalog_page |
integer | no | Page number for catalog (see Catalog events). Default: 1 |
Request:
Response 200:
data/meta/linksare unchanged.catalog/catalog_metaare additional keys (see Catalog events);catalogis an empty list when there is nothing to show. The same filters apply to both lists.
The
urlfield is the public Seatpin storefront page for the event (the same page a buyer sees). It is built from the event's English slug;nullon the rare event that has no slug yet. The same field is returned by/events/details.
Error Responses:
401— Missing or invalid token422— Invalid query parameter (e.g.,end_datebeforestart_date)
GET /api/v1/events/details
Retrieve full details for one or more events by ID, including venue information and the complete map category/section tree needed to create listings.
Authentication: Bearer token required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
event_id |
integer or array | yes, unless catalog_id is sent |
One or more event IDs. Up to 50 per request. |
catalog_id |
string or array | yes, unless event_id is sent |
One or more catalog IDs (e.g. 9b2f6c1e-4d7a-4e3b-8a51-2f0c7d9e1a64). Up to 50 per request. Accepts the same shapes as event_id. |
The API normalizes all of the following shapes into an integer array:
?event_id[]=1042 ← single value (array form)
?event_id[]=1042&event_id[]=1055 ← standard repeated query param
?event_id=1042,1055 ← comma-separated string
?event_id=[1042,1055] ← bracketed string
Request:
Response 200:
Notes:
- Use the
category.idas thecategoryfield andsection.idas thesectionfield when creating listings via/api/v1/inventory/bulk-add. sectionsat the top level is a flat list of all sections across all categories — a convenience for UIs that need a single lookup list.- Events that are not active, hidden, or not published on Seatpin are silently omitted from the response.
event_idandcatalog_idcan be combined in one request; results are returned together indata.- For a catalog event that is not live yet, the entry has
catalog_idinstead ofidandurlisnull. Its category and sectionidvalues are string keys (not integers). See Catalog events. - A
catalog_idthat has already been turned into an event is answered like anevent_id: the entry carries the integeridand integer category/section ids. - Unknown or unavailable
catalog_idvalues are silently omitted.
Error Responses:
401— Missing or invalid token422— neitherevent_idnorcatalog_idgiven, or not an array / malformedcatalog_id
Catalog events
Besides events that are live on Seatpin, the search also lists catalog events: upcoming events you can list tickets on that are not live on Seatpin yet. They are identified by a string catalog_id (an opaque UUID, like "9b2f6c1e-4d7a-4e3b-8a51-2f0c7d9e1a64") instead of an integer id.
data (live events) |
catalog (catalog events) |
|
|---|---|---|
| Identifier | id (integer, permanent) |
catalog_id (string, temporary) |
| Pagination | page → meta |
catalog_page → catalog_meta |
| Categories / sections | integer ids | string keys |
Flow:
- Find the event in
catalogvia/events/searchand read its categories and sections with/events/details?catalog_id[]=9b2f6c1e-4d7a-4e3b-8a51-2f0c7d9e1a64:
- Create the listing with
/inventory/bulk-add, sendingcatalog_idinstead ofevent_id, and the category / section keys from the details response:
The event is created instantly and the response returns its permanent event_id next to the catalog_id:
- From then on use
event_id(and the integer category/section ids from/events/details?event_id[]=1290) for everything.
Things to know:
catalog_idis a temporary reference. Do not store it as a permanent key; store theevent_idreturned bybulk-add. Acatalog_idthat was already turned into an event keeps resolving to that event for a while, butevent_idis the permanent key.- The event moves from
catalogtodatain/events/searchonly once it is published (it goes live on Seatpin). Until then it is not listed indata; it stays reachable through theevent_idreturned bybulk-add, and/events/detailsand the inventory endpoints work with thatevent_id. - The event becomes visible on the Seatpin website shortly after its first published listing. If you create listings with
publish: 0, the event stays hidden. Meanwhile you can manage your listings on it normally through the inventory endpoints. - A single item must carry either
event_idorcatalog_id, not both.
7.2 Inventory
POST /api/v1/inventory/bulk-add
Add one or more new listings to your inventory in a single request. A maximum of 100 listings can be submitted per call.
Authentication: Bearer token required.
Headers:
Authorization: Bearer {api_key}
Content-Type: application/json
Request Body:
Note: You do not send a
listing_idwhen creating listings. Seatpin generates a stable, immutable 5–11 digit numericlisting_idfor each successful row and returns it in the response. Use that value for every subsequent call (update, bulk-update-action, list/get, etc.).
| Field | Type | Required | Description |
|---|---|---|---|
publish |
integer (0 or 1) |
yes | 1 = publish listings immediately; 0 = save as draft |
listings |
array | yes | Array of listing objects. Min 1, max 100. |
listings[].event_id |
integer | yes, unless catalog_id is sent |
Seatpin event ID (from /events/search data or /events/details) |
listings[].catalog_id |
string | yes, unless event_id is sent |
Catalog event ID (from /events/search catalog), e.g. "9b2f6c1e-4d7a-4e3b-8a51-2f0c7d9e1a64". Send either event_id or catalog_id. See Catalog events. |
listings[].category |
integer (string for catalog_id items) |
no | Category ID from event.categories[].id. For catalog items, the category key string. |
listings[].section |
integer (string for catalog_id items) |
no | Section ID from event.categories[].sections[].id. For catalog items, the section key string. |
listings[].row |
string | no | Row identifier (e.g. "A", "12"). Max 50 chars. |
listings[].quantity_available |
integer | yes | Number of tickets in this listing. Min 1. |
listings[].split_type |
string | no | One of Separately, All in one, Dont leave one, Single seats, Sell in multiples. Defaults to Separately. |
listings[].split_quantity |
integer | conditional | Only honoured when split_type is "Sell in multiples". Every other split type is stored with split_quantity = 1. Defaults to 2 when Sell in multiples is used without an explicit quantity. |
listings[].sell_price |
string (decimal) | yes | Per-ticket seller price — what you receive per ticket (e.g. "125.00"). The buyer-facing price is computed and stored separately. |
listings[].sell_price_currency |
string | yes | ISO currency code. Must be one of the external_id values returned by /inventory/options.currencies. |
listings[].ticket_type |
integer | no | external_id from /inventory/options.ticket_types |
listings[].in_hand |
integer (0 or 1) |
no | 1 = tickets are physically in hand |
listings[].restrictions |
array of integers | no | external_ids from /inventory/options.restrictions |
listings[].benefits |
array of integers | no | external_ids from /inventory/options.benefits |
listings[].notes |
array of strings or objects | no | Free-text notes about these specific tickets, for anything the restrictions / benefits catalogues have no entry for. Max 5 per listing, 500 characters each. See below. |
listings[].reference |
string | no | Your own identifier for this listing (max 191 chars). Makes the create idempotent: if you send the same reference again, the existing listing is returned (status: "exists") instead of a duplicate being created. Use it to make retries and re-syncs safe. See below. |
Idempotency via
referenceAreferenceis your stable key for a listing (e.g. the listing's ID in your own system). Areferenceis unique among your active listings. On a repeatbulk-addwith areferencethat matches a live listing, Seatpin does not create a second listing — it returns the existing one with"status": "exists"and the samelisting_id. It does not update the existing listing; to change price/quantity/etc. call/inventory/update(or/inventory/bulk-update-action) with thelisting_id.Deleting a listing frees its
reference. Uniqueness only applies to non-deleted listings, so an unpublish → delete → re-create with the samereferenceis fully supported — the re-create makes a fresh listing. Rows without areferencebehave as before (a new listing every time). Thereferenceis echoed back on all read endpoints (/inventory/list,/inventory/list/get,/inventory/event/{event}/lists).
Delivery There is no separate
delivery_optionsfield. Delivery is implied byticket_type: file-based formats (E-tickets) are fulfilled by uploading files,mobile/mobile-linkformats are fulfilled by sending ticket links (mobile_link[]/ios_link[]/android_link[]), andpaper-ticketorders expose abuyer_addressin/sales/detailsfor physical shipping. All are submitted through/sales/fulfilment.
Free-text notes
restrictionsandbenefitsare a curated catalogue, and no catalogue covers everything a seller needs to say about one particular pair of tickets.notesis where that goes — "the view is partly blocked by a pillar", "collect from the box office before 18:00". Notes are shown to the buyer on the listing and repeated in their order confirmation email, translated into their language, alongside the catalogue options.Two shapes are accepted:
"notes": ["A pillar partly blocks the view of the left of the stage"] "notes": [{"text": "Includes a match programme", "side": "advantage"}]
sideisrestriction(the default) oradvantage, and decides which of the two buyer-facing lists the note joins. Notes are matched by their wording, so the same note sent on a thousand listings is stored and translated once.Write notes in English. They are translated from English into all 36 site languages. A note sent in another language is published as-is and will not translate correctly.
Two things are removed before a note is published, because a note is public and is the obvious place to try to move a sale off-platform: email addresses, links, and international phone numbers are stripped, and anything that is only a contact detail is discarded entirely. HTML is stripped. Keep notes to what the buyer needs to know about the tickets.
Sending
notesreplaces the listing's notes, exactly likerestrictionsandbenefits. Sending"notes": []clears them. Omitting the field leaves them untouched.
Request:
Response 201 (all succeeded):
Rows created from a
catalog_idadditionally returnevent_id(the permanent event the listing now belongs to) and echo thecatalog_id. Useevent_idfrom then on.
Each row's
statusis"created"for a new listing, or"exists"when a row'sreferencematched a listing you already have — in that caselisting_idis the existing one and nothing new was created.
public_idis the identifier your listing appears under on the public seatpin.com site (a UUID), whilelisting_idis the numeric id you use in API calls. They are two different values for the same listing. Usepublic_idto find your own listing among the others in a section when you compare prices — the API returns it on create and on every read (/inventory/get,/inventory/list,/inventory/list/get).
Response 207 (partial success — at least one created, at least one failed):
Response 422 (every row failed):
Error Responses:
401— Invalid or missing token422— Top-level validation failed (e.g.event_iddoes not exist, missing required fields) or every listing in the batch failed
GET /api/v1/inventory/options
Returns all valid values for enumerated listing fields. Use this endpoint to populate dropdown menus in your integration UI.
Authentication: Bearer token required.
Request:
Response 200 (truncated):
About the values
ticket_types,benefits,restrictionsandcurrenciesare pulled from the live database — call this endpoint periodically to pick up new options.external_idforticket_types,benefitsandrestrictionsis an integer (rows in thelisting_formats,advantages, andrequirementstables respectively).external_idforcurrenciesis the ISO code from thecurrenciestable (e.g."GBP"). This is the exact value you must send insell_price_currency.split_typesis a fixed vocabulary and uses string identifiers.
POST /api/v1/inventory/update
Update a single listing by listing_id. Only the fields you include are updated — omitted fields are left unchanged. Ownership is verified; you cannot update another seller's listing.
Authentication: Bearer token required.
Headers:
Authorization: Bearer {api_key}
Content-Type: application/json
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
listing_id |
string | yes | The 5–11 digit numeric identifier returned at create time (immutable) |
event_id |
integer | no | Change the event (use with care) |
category |
integer | no | New category ID |
section |
integer | no | New section ID |
row |
string | no | New row |
quantity_available |
integer | no | New ticket quantity (1–1000) |
split_type |
string | no | Separately, All in one, Dont leave one, Single seats, Sell in multiples. Sending any value other than Sell in multiples automatically resets split_quantity to 1. |
split_quantity |
integer | conditional | Only persisted when the listing's effective split_type is Sell in multiples. Otherwise ignored. |
sell_price |
string (decimal) | no | New per-ticket seller price. Buyer price is recomputed automatically as sell_price * (1 + seller_commission_rate). |
sell_price_currency |
string | no | ISO currency code from /inventory/options.currencies (e.g. "GBP") |
ticket_type |
integer | no | external_id from /inventory/options.ticket_types (writes to listing_format_id) |
benefits |
array of integers | no | external_ids from /inventory/options.benefits (replaces existing set) |
restrictions |
array of integers | no | external_ids from /inventory/options.restrictions (replaces existing set) |
notes |
array of strings or objects | no | Free-text notes (replaces existing set; [] clears them). Same rules as /inventory/bulk-add — see Free-text notes. |
in_hand |
integer (0/1) |
no | Whether tickets are in hand |
publish |
integer (0/1) |
no | Publish (1) or unpublish (0) |
Request:
Response 200:
Error Responses:
401— Invalid or missing token404— Listing not found or does not belong to you422— Validation failed
POST /api/v1/inventory/bulk-update
Perform a bulk status action (publish all, unpublish all, or delete) on multiple listings at once. Accepts multipart form data (matching the Tixstock pattern for compatibility).
Authentication: Bearer token required.
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | yes | publish-all, unpublish-all, or delete |
listing_ids |
string or array | conditional | 5–11 digit listing IDs. Accepts comma-separated string (e.g. "82691538,35283569") or listing_ids[] array form. |
If no IDs are provided with
publish-allorunpublish-all, the action applies to all of your listings. Withdelete,listing_idsis required.
Request:
Response 200:
Error Responses:
401— Invalid or missing token422— Unknown action, ordeletecalled without IDs
POST /api/v1/inventory/bulk-update-action
Update multiple listings with individual field changes in a single request. Supports per-listing field overrides. Returns 207 Multi-Status when some listings fail.
Authentication: Bearer token required.
Headers:
Authorization: Bearer {api_key}
Content-Type: application/json
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
publish |
integer (0/1) |
no | Global publish override applied to all listings in this call |
listings |
array | yes | Array of listing update objects (same fields as /inventory/update) |
listings[].listing_id |
string | yes | The 5–11 digit numeric listing identifier returned at create time |
listings[].sell_price |
string (decimal) | no | New price |
listings[].sell_price_currency |
string | no | New currency |
listings[].quantity_available |
integer | no | New quantity |
listings[].split_type |
string | no | Separately, All in one, Dont leave one, Single seats, Sell in multiples |
listings[].ticket_type |
integer | no | external_id from /inventory/options.ticket_types |
listings[].publish |
integer (0/1) |
no | Per-listing publish override (overridden by global publish if set) |
| (any other update field) | — | no | See /inventory/update field table |
Request:
Response 200 (all succeeded):
The
payloadmap echoeslisting_id → listing_idto confirm that each requested update was applied to the correct row.is_ticket_type_changeflags whether the listing'sticket_typewas modified (1) or unchanged (0).
Response 207 (partial failure):
Response 422 (every row failed):
POST /api/v1/inventory/get
Retrieve a paginated list of events that have at least one active listing belonging to you.
Authentication: Bearer token required.
Headers:
Authorization: Bearer {api_key}
Content-Type: application/json
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
event_name |
string | no | Partial match on event name |
start_date |
date (YYYY-MM-DD) |
no | Filter events on or after this date |
end_date |
date (YYYY-MM-DD) |
no | Filter events on or before this date |
per_page |
integer | no | Results per page. Default: 20 |
page |
integer | no | Page number. Default: 1 |
Request:
Response 200:
POST /api/v1/inventory/event/{event}/lists
Get a paginated list of your listings for a specific event. The {event} path parameter is the Seatpin event ID.
Authentication: Bearer token required.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
event |
integer | Seatpin event ID |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page |
integer | no | Default: 20 |
page |
integer | no | Default: 1 |
Body Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | no | Filter by status: active, inactive, pending |
category_id |
integer | no | Filter by map category ID |
Request:
Response 200:
Error Responses:
404— Event not found, not active, or not published on Seatpin
POST /api/v1/inventory/list
Flat, paginated list of all your listings in the full detail shape — built for periodic inventory reconciliation. Same row shape as /inventory/event/{event}/lists, but across every event in one call, and with an updated_after delta filter.
Use it like this: on each pass, send updated_after set to the largest updated_at you saw last time. When nothing changed you get an empty page (one cheap request); when something did, you get only the changed rows. With per_page up to 500 a full sync of thousands of listings is a handful of requests instead of one-per-event.
Authentication: Bearer token required.
Body Parameters: (all optional)
| Field | Type | Description |
|---|---|---|
updated_after |
string | Only listings changed after this timestamp. ISO 8601 (2026-08-18T09:00:00Z) or Y-m-d H:i:s. Echo back the largest updated_at from the previous pass — no timezone math needed on your side. |
is_active |
boolean | Filter by publish state (true = published, false = unpublished). |
event_id |
integer | Restrict to a single event. |
per_page |
integer | 1–500. Default 100. |
page |
integer | Default 1. |
Results are ordered by updated_at ascending (then id), so the last row of the last page carries the timestamp to use as your next updated_after.
Request:
Response 200:
referenceis the seller-side identifier you sent onbulk-add(see below);nullif none was given. Soft-deleted listings are not returned, so a listing that disappears from this list has been removed.
GET /api/v1/inventory/list/get
Retrieve a single listing by its listing_id.
Authentication: Bearer token required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
listing_id |
string | yes | The 5–11 digit numeric identifier returned at create time |
Request:
Response 200:
Reading back your edits
ticket_type,split_type,split_quantity,benefitsandrestrictionsare returned in exactly the shapes the write endpoints accept, so a read directly confirms whether an edit propagated.
notesis the one exception: it is written as plain strings but read back as objects, because the sanitised wording can differ from what you sent and you need to see what is actually published."notes": [ {"id": 481, "text": "A pillar partly blocks the view of the left of the stage", "side": "restriction", "status": "approved"} ]
statusisapproved(shown to buyers),pending(held for review) orrejected(withheld). A rejected note stays on orders that were already sold with it. Note the pricing asymmetry:face_valuein read responses is the seller price you submitted assell_priceat write time; the read-sidesell_priceis the buyer-facing price (face_value * (1 + seller_commission_rate)).
Error Responses:
404— Listing not found or does not belong to you422—listing_idis missing or not in the 5–11 digit numeric format
POST /api/v1/inventory/counts
Get aggregate listing counts for your account. Optionally scoped to a specific event.
Authentication: Bearer token required.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
event_id |
integer | no | Scope counts to a specific event |
Request:
Response 200:
Field Definitions:
| Field | Description |
|---|---|
total |
All listings including deleted |
published |
Active (is_active=1), approved (approval=1), not deleted |
unpublished |
Inactive (is_active=0), not deleted |
deleted |
Soft-deleted listings |
pending_approval |
Not yet approved by Seatpin admin, not deleted |
7.3 Sales
POST /api/v1/sales/listing
Retrieve a paginated list of your sales (orders placed against your listings).
Authentication: Bearer token required.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
date_from |
date (YYYY-MM-DD) |
no | Include orders created on or after this date |
date_to |
date (YYYY-MM-DD) |
no | Include orders created on or before this date. Must be >= date_from |
status |
string | no | Filter by derived order status: pending, approved, cancelled, refunded, fulfilled. (confirmed and completed are accepted as legacy synonyms of approved / fulfilled.) |
per_page |
integer | no | Default: 20, max: 100 |
page |
integer | no | Default: 1 |
Request:
Response 200:
About
order_id: This is the public-facing order identifier — a string of ~10 digits (Seatpin'sorder_code, e.g."4820916377"). The internal numeric primary key is never exposed by this API. The same value arrives in theorder.created/payment.confirmedwebhooks, and it is the exact value you pass to/sales/detailsand/sales/fulfilment.
POST /api/v1/sales/details
Retrieve full details for a single order including buyer PII appropriate to the ticket format.
Authentication: Bearer token required.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
order_id |
string | yes | The order code returned by /sales/listing (e.g. "4820916377") |
PII Matrix by Ticket Format:
| Format | PII Fields Returned |
|---|---|
eticket / default |
first_name, last_name |
mobile |
first_name, last_name, email |
pdf |
first_name, last_name |
paper-ticket / paper-ticket-lms |
name (full name) + top-level buyer_address object |
passolig-tickets / passolig-tr-tickets |
name, nationality, national_id, birth_place, dob |
| Italy football events (any format) | Same as passolig above |
Request:
Response 200 (e-ticket order — captured from a live order, customer names anonymised):
Field notes
- The
customerobject is nested inside eachticket_details[]entry — one entry per ticket. When buyer names have not been collected yet,customerisnull(the entry itself is still present).statusis always populated and derived from the real order state — see Order Lifecycle, Statuses & When to Act.listing.category/section/row/ticket_formatcome from the order, not the listing: they are snapshotted at sale time and stay fixed for the lifetime of the order, even if the listing is later edited or deleted. Clients get exactly what the buyer purchased.
Response 200 (paper ticket order — includes top-level buyer_address):
Error Responses:
401— Invalid or missing token404— Order not found or does not belong to you
POST /api/v1/sales/fulfilment
Deliver an order to the buyer. You can fulfil in one of three ways: upload ticket files, provide mobile ticket links (for mobile / mobile-link formats), or supply shipping tracking for physical tickets. Accepts multipart form data.
Files are stored in S3 private storage at Seller-tickets/{user_id}/{order_id}/{uuid}.{ext}. After submitting, a Seatpin admin manually reviews and confirms delivery — is_sent and is_delivered flags are not automatically set.
Authentication: Bearer token required.
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
order_id |
string | yes | The order code returned by /sales/listing (e.g. "4820916377") |
files[] |
file | no* | Ticket file(s) to upload. Accepted formats: PDF, JPG, PNG. Max 10 files, 10 MB each. |
mobile_link[] |
url | no* | Universal mobile ticket links (one URL that opens on any device). Max 10, each a valid URL up to 2048 chars. |
ios_link[] |
url | no* | iOS-specific ticket links (e.g. Apple Wallet). Max 10, each a valid URL up to 2048 chars. |
android_link[] |
url | no* | Android-specific ticket links (e.g. Google Wallet). Max 10, each a valid URL up to 2048 chars. |
tracking_number |
string | no* | Shipping tracking number (max 191 chars) |
shipping_company |
string | no* | Shipping company name (max 100 chars). Required when tracking_number is provided. |
*At least one delivery method must be provided: files[], or any of the mobile link arrays (mobile_link[] / ios_link[] / android_link[]), or both tracking_number and shipping_company. Providing only tracking_number without shipping_company (or vice versa) returns a validation error.
How mobile links map to tickets: the link arrays are distributed 1:1 by index to the individual tickets in the order. mobile_link[0], ios_link[0] and android_link[0] all apply to the first ticket, [1] to the second, and so on — so you can attach a universal link, an iOS link and an Android link to the same ticket at once. Send links in the same order the tickets appear in /sales/details. Blank array entries are skipped, so you can target specific tickets by leaving earlier slots empty (e.g. mobile_link[0]=""&mobile_link[1]=https://...).
Request (file upload):
Request (mobile ticket links):
Request (shipping tracking):
Response 200:
Error Responses:
401— Invalid or missing token404— Order not found or does not belong to you422— No delivery method provided (no files, no mobile links, no tracking details); or cancelled/refunded order422— File too large, wrong format, or too many files; or a mobile link is not a valid URL / exceeds 2048 chars
GET /api/v1/sales/sales-options
Returns all valid values for order status and shipping fields. Use to populate dropdowns in your integration.
Authentication: Bearer token required.
Request:
Response 200:
7.4 Reports
POST /api/v1/reports/details
Retrieve a sales summary report grouped by a time period (day/week/month/year). Dates are timezone-aware using the server's configured timezone.
Authentication: Bearer token required.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
period |
string | yes | Grouping granularity: day, week, month, or year |
start_date |
date (YYYY-MM-DD) |
no | Report start date. Default: 30 days ago. Max range: 365 days. |
end_date |
date (YYYY-MM-DD) |
no | Report end date. Default: today |
event_id |
integer | no | Scope report to a specific event |
Request:
Response 200:
Period Format by Granularity:
| Period | Format | Example |
|---|---|---|
day |
YYYY-MM-DD |
2026-06-09 |
week |
YYYY-WW (ISO week) |
2026-24 |
month |
YYYY-MM |
2026-06 |
year |
YYYY |
2026 |
Notes:
- Refunded orders are excluded from all revenue calculations.
currencyin the summary is"mixed"when your orders span multiple currencies.
POST /api/v1/reports/listing
Retrieve a paginated per-listing performance report. Useful for identifying your top-performing listings.
Authentication: Bearer token required.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
event_id |
integer | no | Scope to a specific event |
start_date |
date (YYYY-MM-DD) |
no | Default: 90 days ago |
end_date |
date (YYYY-MM-DD) |
no | Default: today |
per_page |
integer | no | Default: 20 |
page |
integer | no | Default: 1 |
sort_by |
string | no | Sort field: revenue (default), tickets_sold, created_at |
sort_direction |
string | no | asc or desc (default: desc) |
Request:
Response 200:
GET /api/v1/reports/reports-options
Returns all valid values for report configuration fields.
Authentication: Bearer token required.
Request:
Response 200:
GET /api/v1/reports/trending-events
A demand-and-opportunity signal: which upcoming events are getting the most orders on the marketplace right now, so you can decide where to add inventory. It is deliberately number-free — events are ranked by total order volume (every order, paid or not), but no counts are ever returned. You get the ranking plus a coarse supply tier and per-seller opportunity flags.
Authentication: Bearer token required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
period |
string | no | Window to rank over: 7d or 30d. Default 7d. |
Response fields (per event, ordered by demand, hottest first):
| Field | Type | Description |
|---|---|---|
rank |
integer | Demand position, 1 = most orders in the window. |
event |
object | id, name, date, venue, url. |
supply |
string | Coarse competition tier from the number of active Seatpin listings for the event (low, medium, high). Counts our own sellers only — separate storefronts and external provider inventory are excluded. |
you_are_listed |
boolean | Whether you already have a listing for this event. |
opportunity |
boolean | true when supply is low and you are not listed — high demand, thin competition, and you're not there yet. |
At most 100 events are returned. Because demand moves slowly relative to the weekly/monthly windows, the ranking is cached and refreshed roughly once a day.
Request:
Response 200:
No order counts, ticket counts or listing counts are exposed — only the rank and tiers. Filter the list to
opportunity: trueto find the events worth listing into first.
7.5 Sandbox Order Simulation
Sandbox only. Every endpoint in this section returns
404unless the environment runs in sandbox mode.
Place a test order against your own published listing and walk it through the full lifecycle. Each step writes real rows and fires the exact same observers as a storefront sale, so:
- the real
order.created,order.updated,order.cancelled,payment.confirmedandpayment.refundedwebhooks are dispatched to your configured endpoints (inspect request/response in Portal -> Webhook Logs); - the order shows up in
POST /sales/listingandPOST /sales/detailswith the exact production payload shapes (including nestedticket_details[].customerobjects, populated with fake buyer names).
Typical flow:
POST /api/v1/sandbox/orders -> (no webhook yet) (status: pending)
POST /api/v1/sandbox/orders/{order_id}/pay -> order.created + payment.confirmed + order.updated (status: approved)
POST /api/v1/sandbox/orders/{order_id}/cancel -> order.cancelled (status: cancelled)
POST /api/v1/sandbox/orders/{order_id}/refund -> payment.refunded + order.updated (status: refunded)
order.createdmirrors production semantics: it fires when the payment reaches step-1 approval (the/paystep), not when the order row is first created.
POST /api/v1/sandbox/orders
| Field | Type | Required | Description |
|---|---|---|---|
listing_id |
string | yes | Your listing's 5-11 digit identifier. Must be published (publish=1) and belong to you. |
quantity |
integer | no | Tickets to "buy". Default 1, max 10, cannot exceed the listing's availability. |
Response 201:
Use the returned order_id directly with POST /sales/details to capture the full order payload.
POST /api/v1/sandbox/orders/{order_id}/pay
Creates a payment and advances it to step-1 approval in one call. Fires order.created, payment.confirmed and order.updated (status approved). Returns 422 if already paid.
POST /api/v1/sandbox/orders/{order_id}/cancel
Sets the order status to cancelled. Fires order.cancelled. Returns 422 if already cancelled.
POST /api/v1/sandbox/orders/{order_id}/refund
Requires a confirmed payment (/pay first). Marks the full amount refunded — fires payment.refunded and order.updated (status refunded). Returns 422 if not paid yet or already refunded.
Notes
{order_id}is the public order code returned at creation; these endpoints only ever operate on orders created via/sandbox/orders— real orders cannot be touched.- Sandbox orders are purged automatically after a retention period (default 7 days).
7.6 Payouts
GET /api/v1/payouts
Per-order payout / reconciliation status for your account: which of your sales are still owed to you, which are on their way, and which have already been paid — plus a summary of the amounts in each state. It returns exactly what the seller-area Payouts page shows, so the API and the portal never disagree.
Scoped to your API key, like every other endpoint. If you manage sub-sellers, call it with a sub-seller's key to get that sub-seller's payouts.
Authentication: Bearer token required.
Query parameters:
| Field | Type | Required | Description |
|---|---|---|---|
status |
string | no | Filter to one payout state (see below). Omit for all. |
event_id |
integer | no | Only payouts for this event. |
start_date |
date (YYYY-MM-DD) |
no | Events on or after this date. |
end_date |
date (YYYY-MM-DD) |
no | Events on or before this date. |
per_page |
integer | no | Results per page. Default 20, max 50. |
page |
integer | no | Page number. Default 1. |
Payout states (status):
| status | meaning |
|---|---|
paid |
Money has left — a provider transfer id is attached (payout_reference). |
processing |
Claimed by a payout batch that is on its way; not yet confirmed. |
due |
Settled and the event has passed — goes out in the next payout run. |
scheduled |
Settled, but the event is still in the future — payouts wait for the event. |
awaiting_fulfilment |
You have not delivered the tickets yet. |
awaiting_approval |
Tickets are in; the transfer has not been approved yet. |
on_hold |
The buyer's payment itself is not fully approved yet. |
not_payable |
Refunded, rejected or declined — will not be paid out. |
Request:
Response 200:
payout_referenceis the provider transfer id to match against your bank/provider statement —nulluntil the money has actually left.paid_atis set only whenstatusispaid.- Amounts are the seller side only (
seller_price/seller_currency) — what we pay you. Buyer totals and commission are never returned.
8. Webhooks
Webhooks allow Seatpin to push real-time event notifications to your server. You configure one or more webhook endpoints in the Portal, and Seatpin sends an HTTP POST to your URL whenever a subscribed event occurs.
Configuring Webhooks
- Go to Portal → Webhooks → Add webhook
- Enter your endpoint URL (must be a publicly reachable HTTPS URL)
- Select the event types you want to receive
- Save — the secret is shown once; copy it immediately
- To rotate a secret: Portal → Webhooks → Regenerate secret
Limits: Up to 20 webhook endpoints per account (configurable via Seller_WEBHOOK_MAX_PER_USER).
Delivery Format
All webhooks are sent as HTTP POST with the following headers:
Content-Type: application/json
User-Agent: Seatpin-Seller-Webhook/1.0
X-Seller-Event: listing.created
X-Seller-Signature: sha256=<hmac_hex>
X-Seller-Timestamp: 1749466800
X-Seller-Delivery: 9821
Body envelope:
Verifying Signatures
The X-Seller-Signature header is sha256= followed by the hex HMAC-SHA256 of the raw request body using your webhook secret.
Always verify signatures to ensure requests are genuinely from Seatpin and have not been tampered with.
Node.js:
Python:
PHP:
Important: Always read
rawBodybefore JSON-parsing. Many frameworks consume the body stream during parsing, leaving an empty string for HMAC calculation.
Order Lifecycle, Statuses & When to Act
data.status (and the status in /sales/listing + /sales/details) is derived from the real order state and is always populated:
| status | meaning |
|---|---|
pending |
Payment received but not yet verified by Seatpin |
approved |
Payment verified (step-1 approval) — safe to register the order and deliver tickets |
fulfilled |
Tickets delivered to the buyer |
cancelled |
Payment declined or order cancelled |
refunded |
Refund confirmed |
When to act:
- Treat
order.createdas "the sale is confirmed" — it fires exactly once per order, at the moment the order reachesapproved. Register the order and start fulfilment on this event. payment.confirmedmay fire more than once (each approval step); it is informational — you do not need to act on it if you key offorder.created.order.updatedfires on later state changes — readdata.statusfrom it.- Cancellations and refunds have dedicated events:
order.cancelled(payment declined or order cancelled) andpayment.refunded(refund confirmed; the order'sstatusalso becomesrefunded).
Event Types and Payload Schemas
listing.created
Fired when a new listing is created.
listing.updated
Fired when a listing's fields change.
listing.deleted
Fired when a listing is soft-deleted.
order.created
Fired when an order becomes confirmed for you — i.e. when its payment reaches step-1 approval (Seatpin verifies the buyer's payment). It is intentionally not fired at raw checkout time, so you never hear about orders whose payment was never verified. Sent once per order.
order_id is the same order code that POST /sales/details accepts — pass it straight through to fetch full order details (including buyer info). listing_id is the listing's public identifier. seller_price is the seller-side order total in seller_currency — the amount you will receive. The example below is the same order shown in the /sales/details example above.
sub_seller is null for your own sales. If you use sub-sellers (see section 9), an order belonging to one carries a sub_seller object so you can route the event to the right account:
id is the sub-seller's account id (the same id returned by GET /api/v1/sub-sellers) and reference is the label you assigned to that sub-seller. The sub_seller field is present on order.*, payment.confirmed and payment.refunded payloads.
order.updated
Fired when an order's fields change (except a change to cancelled, which fires order.cancelled instead). Same payload shape as order.created — status and updated_at carry the new values.
order.cancelled
Fired when an order's status changes to cancelled. Same payload shape as order.created.
ticket.hold
Fired when tickets are put on hold (reserved for a buyer during checkout).
ticket.release
Fired when a hold is released (checkout abandoned or timed out).
payment.confirmed
Fired whenever the payment advances through a confirmation step (payment received, or an approval step is completed). You may receive this event more than once per order — treat deliveries idempotently, keyed on order_id.
order_id is the same order code that POST /sales/details accepts. seller_price is the seller-side order total in seller_currency — the amount you will receive. The example below is the payment for the same order shown above.
payment.refunded
Fired when a refund is confirmed on the payment. Same base payload as payment.confirmed, plus refund_status.
webhook.test
Fired when you trigger a test delivery from the Portal.
Retry Policy
- Delivery is considered successful when your endpoint responds with any
2xxHTTP status code. - If delivery fails, the system retries 3 times total with exponential back-off:
- Attempt 1: immediate
- Attempt 2: +60 seconds
- Attempt 3: +300 seconds
- Attempt 4 (final): +900 seconds
- Auto-disable: After 10 consecutive delivery failures the webhook is automatically disabled, and a notification email is sent to your account email.
- Re-enable: Portal → Webhooks → Edit → Enable. Fix the underlying issue before re-enabling.
- Manual retry: Portal → Webhook Logs → select a failed delivery → Retry.
- Timeout: Each delivery attempt has a 30-second timeout.
DNS Rebinding Protection
The webhook delivery system validates that your configured URL resolves to a public IP address at the time of each delivery attempt. URLs resolving to private RFC 1918 ranges (e.g., 192.168.x.x, 10.x.x.x) or loopback addresses are rejected and counted as a failure.
9. Sub-sellers
If you run an agency or resell on behalf of other sellers, you can register them as sub-sellers under your account. Each sub-seller is a full, independent seller on Seatpin — they pass their own identity (KYC) and bank verification and are paid directly by Seatpin. Settlement between you and your sub-sellers is your own business; Seatpin is not involved.
You keep full control of the integration: sub-sellers never touch the API. You create and hold each sub-seller's API key and configure their webhooks, all from the Portal.
How it works
- Invite — In Portal → Sub-sellers, create a reusable invite link and share it with your seller. They sign up at
https://www.seatpin.com/signup?invite=<token>and are automatically linked to you (no approval step). You can revoke a link at any time; already-joined sub-sellers are unaffected. - Provision — Open a sub-seller from Portal → Sub-sellers → Manage to:
- create/revoke that sub-seller's API keys (each key acts as that sub-seller — every call scopes to their listings and orders),
- add/remove their webhooks,
- set a reference — your own label for that sub-seller, echoed back in webhook payloads.
- Operate — Use the sub-seller's API key to manage their inventory and read their sales, exactly as you would your own. There is nothing sub-seller-specific in the endpoint calls themselves — the key determines whose data you touch.
Mapping events back to a sub-seller
Every order.*, payment.confirmed and payment.refunded webhook for a sub-seller's order carries a sub_seller object:
id— the sub-seller's Seatpin account id (stable).reference— the label you set in the Portal (ornull).
For your own sales the field is null.
GET /api/v1/sub-sellers
Lists the sub-sellers linked to the authenticated master account — use it to build the mapping between sub_seller.id and your own records. Called with your own (master) API key. A sub-seller's key returns an empty list.
Response: