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
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. Default: 1 |
Request:
Response 200:
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 (at least one) | One or more event IDs. Up to 50 per request. |
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.
Error Responses:
401— Missing or invalid token422—event_idparam missing or not an array
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 | Seatpin event ID (from /events/search or /events/details) |
listings[].category |
integer | no | Category ID from event.categories[].id |
listings[].section |
integer | no | Section ID from event.categories[].sections[].id |
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 |
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.
Request:
Response 201 (all succeeded):
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) |
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
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. 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:
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).
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.
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.