When NODE_ENV is not production, Swagger UI is available at /docs and
the generated OpenAPI document at /docs-json. The schema is generated from
controllers and DTOs at startup. Swagger is disabled in production so the
interactive schema is not exposed publicly.
Every route below is real: paths come from the controllers, request
bodies from the class-validator DTOs, and response bodies from what the
services return. When they drift, the generated Bruno collection under
bruno/ fails CI, so treat this file and that
collection as two views of the same source.
Paths are shown without a prefix. When API_PREFIX is set (e.g.
api/v1), every route except GET /health is served under it — see
docs/CONFIGURATION.md.
The examples assume:
API=http://localhost:3000
TOKEN=<accessToken from /v1/auth/login>Offset-paginated listings take ?page=&limit= (PaginationQueryDto,
src/common/dto/pagination-query.dto.ts):
| Param | Default | Rules |
|---|---|---|
page |
1 |
integer ≥ 1 (1-based) |
limit |
20 |
integer 1–100 |
An out-of-range value is rejected with 400. The response body is a
Paginated<T> (src/common/pagination/paginated.ts). page is also capped at 100000:
{ "items": [ ... ], "total": 57, "page": 2, "limit": 20 }total counts matching rows across all pages. A page past the end
returns items: [] with the real total.
Currently paginated: GET /v1/events (published events, ordered by
startsAt then id so rows don't shift between pages).
GET /tickets/resale uses cursor pagination instead
(?cursor=&limit= → { items, nextCursor, limit }).
To paginate a new endpoint, accept @Query() query: PaginationQueryDto,
have the service return prisma.$transaction([findMany({ ...toSkipTake(query) }), count()]),
and add PaginatedResponseInterceptor to the handler's @UseInterceptors(...). The
interceptor turns the [items, total] result into a Paginated<T> body.
Handlers can also build the body directly with paginate(items, total, query).
Every GET response carries a weak ETag (W/"..."), computed by
Express from the response body (app.set('etag', 'weak') in
src/app.setup.ts). A client that re-sends it as If-None-Match gets
304 Not Modified with an empty body while the response is unchanged,
so polling GET /v1/events does not re-download an identical list:
GET /v1/events
→ 200 ETag: W/"1a2-Lx0..."
GET /v1/events
If-None-Match: W/"1a2-Lx0..."
→ 304 (no body)The ETag is a hash of the serialized response, so it changes whenever
any event on the page, or the page's total, changes. The server still
runs the query to compute it: the saving is bandwidth and client-side
parsing, not database work.
Versioning. All routes live under /v1 (URI versioning, default
version 1).
Authentication. Everything except /v1/health, /v1/industries,
/v1/auth/*, GET /v1/events, GET /v1/events/:eventId and
/v1/stellar/* needs Authorization: Bearer <JWT>. Scanner devices
authenticate POST /v1/tickets/:ticketId/scanner-check-in with their own
device token instead (see that route).
Bodies. Send Content-Type: application/json. The global
ValidationPipe runs with whitelist, forbidNonWhitelisted and
transform, so a property that is not in the DTO is a 400, not ignored,
and query-string numbers are coerced.
Big numbers. Prices and on-chain ids are BigInt columns
(price, chainEventId, chainTicketId, royaltyFee,
sellerProceeds). They are strings in JSON both ways: send "2500000",
receive "2500000". Prices are in the settlement token's smallest unit.
Dates. ISO-8601 strings in UTC, e.g. 2026-12-01T20:00:00.000Z.
Status codes. POST returns 201 unless noted, GET, PATCH and
DELETE return 200. A handler that returns nothing sends an empty
body with that status.
Idempotency. Every build endpoint (POST /v1/tickets/issue,
purchase, :ticketId/transfer, check-in, revoke, list-resale,
cancel-resale, buy-resale) accepts an optional Idempotency-Key
header. A retry with the same key from the same user within
IDEMPOTENCY_KEY_TTL_MINUTES (default 15) replays the first response
instead of building a second envelope.
The build / confirm pair. Each on-chain action is two calls. The
build call returns { "unsignedXdr": "AAAAAgAA..." }, the caller's
wallet signs it, and the matching confirm-* call takes the result back
as signedXdr. Every confirm-* body extends ConfirmSignedTxDto:
| field | type | rules |
|---|---|---|
signedXdr |
string | required, the wallet-signed envelope, unmodified |
txHash |
string | optional, 64 hex characters, when the client already knows the hash |
The examples below abbreviate XDR as "AAAAAgAA...".
Errors. Four shapes, all JSON:
See ERROR_HANDLING.md.
Interactive docs. Outside production, Swagger UI is served at /docs.
Public liveness probe.
curl -s "$API/v1/health"{ "status": "ok", "service": "stellar-tickets-backend" }Public. The Industry enum used by Organization.industry and
Event.category.
curl -s "$API/v1/industries"["CONCERTS", "FLIGHTS", "SPORTS", "FESTIVALS", "CONFERENCES", "BUS", "MOVIE_THEATERS", "MUSEUMS", "TOURIST_ATTRACTIONS", "PUBLIC_TRANSPORT", "UNIVERSITIES", "CORPORATE_EVENTS"]Public. Creates an ATTENDEE account and returns a JWT.
| field | type | rules |
|---|---|---|
email |
string | required, valid email |
password |
string | required, at least 10 characters |
name |
string | required, 1 to 256 characters after trimming |
curl -s -X POST "$API/v1/auth/register" \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"correct-horse-battery-staple","name":"Ada Lovelace"}'{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": "9d3a5c1e-7b2f-4e8a-9c6d-1f2e3a4b5c6d", "email": "ada@example.com", "name": "Ada Lovelace", "role": "ATTENDEE" }
}409 when the email is already registered.
Public. Returns 200.
| field | type | rules |
|---|---|---|
email |
string | required, valid email |
password |
string | required |
curl -s -X POST "$API/v1/auth/login" \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"correct-horse-battery-staple"}'Same body as register. 401 on a wrong email or password.
curl -s "$API/v1/users/me" -H "Authorization: Bearer $TOKEN"{
"id": "9d3a5c1e-7b2f-4e8a-9c6d-1f2e3a4b5c6d",
"email": "ada@example.com",
"name": "Ada Lovelace",
"role": "ATTENDEE",
"stellarPublicKey": "GBAHZWO3UI3GAHPQCPSW6IR5N7HJ4UBRZNAFMSYB6DAKVNHQDOZIV2YJ",
"createdAt": "2026-09-27T12:00:00.000Z"
}Links the caller's Stellar account. Required before any ticket action.
| field | type | rules |
|---|---|---|
stellarPublicKey |
string | required, G... ed25519 public key with a valid checksum |
curl -s -X PATCH "$API/v1/users/me/wallet" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"stellarPublicKey":"GBAHZWO3UI3GAHPQCPSW6IR5N7HJ4UBRZNAFMSYB6DAKVNHQDOZIV2YJ"}'{ "id": "9d3a5c1e-7b2f-4e8a-9c6d-1f2e3a4b5c6d", "stellarPublicKey": "GBAHZWO3UI3GAHPQCPSW6IR5N7HJ4UBRZNAFMSYB6DAKVNHQDOZIV2YJ" }409 when another account already uses that wallet.
Updates the caller's display name and returns the updated profile.
| field | type | rules |
|---|---|---|
name |
string | required, 1 to 256 characters after trimming |
curl -s -X PATCH "$API/v1/users/me" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Ada Lovelace"}'Resolves a recipient before an issue or transfer. email must be a
valid email address.
curl -s "$API/v1/users/lookup?email=grace@example.com" -H "Authorization: Bearer $TOKEN"{ "id": "5e2b9f0c-1a3d-4b7e-8f9a-0c1d2e3f4a5b", "name": "Grace Hopper", "email": "grace@example.com", "stellarPublicKey": "GD6ROJBYLKQMOW3E7N4M2YBPUHMZD7PL65VRHRMO24BOVSBV5H3BQRSL" }404 when no account has that email.
An organization row looks like this everywhere it is returned:
{
"id": "2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b",
"name": "Fillmore Live",
"slug": "fillmore-live",
"industry": "CONCERTS",
"stellarAccount": "GDWUSKGGFDI4FRXK5EBTRECZSVQSSWJHHJOGH6JWG3AUMFFMQ435DIAG",
"logoUrl": null,
"websiteUrl": "https://fillmore.example.com",
"deletedAt": null,
"createdAt": "2026-09-27T12:05:00.000Z",
"updatedAt": "2026-09-27T12:05:00.000Z"
}Creates the organization, makes the caller its OWNER, and promotes an
ATTENDEE caller to ORGANIZER.
| field | type | rules |
|---|---|---|
name |
string | required, 2 to 256 characters after trimming |
slug |
string | required, lowercase letters, digits and single hyphens, up to 128 characters, unique |
industry |
string | required, one of GET /v1/industries |
stellarAccount |
string | required, the organizer's G... public key that signs issue, check-in and revoke transactions |
logoUrl |
string | optional, URL with protocol |
websiteUrl |
string | optional, URL with protocol |
curl -s -X POST "$API/v1/organizations" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Fillmore Live","slug":"fillmore-live","industry":"CONCERTS","stellarAccount":"GDWUSKGGFDI4FRXK5EBTRECZSVQSSWJHHJOGH6JWG3AUMFFMQ435DIAG","websiteUrl":"https://fillmore.example.com"}'409 when the slug is taken.
Organizations the caller belongs to, newest first, soft-deleted ones excluded. Returns an array of organization rows.
curl -s "$API/v1/organizations/mine" -H "Authorization: Bearer $TOKEN"One organization row. 404 when missing or soft-deleted.
curl -s "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b" -H "Authorization: Bearer $TOKEN"Soft delete (members only). Returns the row with deletedAt set.
curl -s -X DELETE "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b" -H "Authorization: Bearer $TOKEN"Clears deletedAt (members only). Returns the row.
curl -s -X POST "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/restore" -H "Authorization: Bearer $TOKEN"Invites an email address to join the organization (OWNER/ADMIN only).
The person doesn't need an account yet. The raw token is returned once;
only its hash is stored, so deliver it to the invitee yourself (e.g. in an
accept link). Invites expire 7 days after creation.
| field | type | rules |
|---|---|---|
email |
string | required, trimmed and lower-cased |
role |
string | optional, ADMIN or STAFF (default STAFF) |
curl -s -X POST "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/invitations" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"email":"grace@example.com","role":"ADMIN"}'{
"invitation": {
"id": "7a1c3e5f-2b4d-4c6e-8f0a-1b2c3d4e5f6a",
"organizationId": "2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b",
"email": "grace@example.com",
"role": "ADMIN",
"invitedById": "5e2b9f0c-1a3d-4b7e-8f9a-0c1d2e3f4a5b",
"expiresAt": "2026-10-25T12:00:00.000Z",
"acceptedAt": null,
"acceptedById": null,
"createdAt": "2026-10-18T12:00:00.000Z"
},
"token": "inv_3f9c…"
}403 when the caller isn't an OWNER/ADMIN, 404 when the organization is
missing or soft-deleted, 409 when that email already belongs to a member.
Accepts an invite as the signed-in user, whose account email must match the
invite's. Creates the OrganizationMember with the invited role and promotes
an ATTENDEE to ORGANIZER. Returns the membership row.
| field | type | rules |
|---|---|---|
token |
string | required, the token from the invite |
curl -s -X POST "$API/v1/organizations/invitations/accept" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"token":"inv_3f9c…"}'410 when the invite has expired, 404 for an unknown token or a
soft-deleted organization, 403 when the email doesn't match, 409 when the
invite was already accepted or the caller is already a member.
All three routes require membership in :organizationId. See
WEBHOOKS.md for delivery and signing.
| field | type | rules |
|---|---|---|
url |
string | required, http or https URL |
events |
string | optional, event filter, up to 512 characters; defaults to * |
secret |
string | optional, up to 256 characters; generated (32 random bytes, hex) when omitted |
curl -s -X POST "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/webhooks" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/webhook","events":"ticket.issued,ticket.checked_in"}'{
"id": "5d7f9b1c-3e4a-4b6c-8d0e-1f2a3b4c5d6e",
"organizationId": "2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b",
"url": "https://example.com/webhook",
"secret": "3f9c1b2a4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8",
"events": "ticket.issued,ticket.checked_in",
"isActive": true,
"createdAt": "2026-09-27T12:10:00.000Z",
"updatedAt": "2026-09-27T12:10:00.000Z"
}Array of endpoint rows, newest first.
curl -s "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/webhooks" -H "Authorization: Bearer $TOKEN"Returns the deleted row. 404 when the endpoint is not in that
organization.
curl -s -X DELETE "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/webhooks/5d7f9b1c-3e4a-4b6c-8d0e-1f2a3b4c5d6e" \
-H "Authorization: Bearer $TOKEN"An event row:
{
"id": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"organizationId": "2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b",
"name": "Launch Night",
"category": "CONCERTS",
"venue": "The Fillmore",
"startsAt": "2026-12-01T20:00:00.000Z",
"endsAt": "2026-12-01T23:00:00.000Z",
"chainEventId": null,
"publishedTxHash": null,
"maxResaleMultiplierBps": 11000,
"royaltyBps": 500,
"status": "DRAFT",
"deletedAt": null,
"createdAt": "2026-09-27T12:15:00.000Z",
"updatedAt": "2026-09-27T12:15:00.000Z"
}A ticket type row:
{
"id": "4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e",
"eventId": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"name": "General Admission",
"price": "2500000",
"quantityTotal": 500,
"quantityIssued": 0,
"saleStartsAt": null,
"saleEndsAt": null,
"isHidden": false,
"createdAt": "2026-09-27T12:16:00.000Z"
}Public marketplace listing: one page (?page=&limit=, see
Pagination) of PUBLISHED events of live organizations,
soonest first, each with its visible ticket types and the organizer's
name and slug. Served with Cache-Control: public, max-age=60, s-maxage=300
and an application cache (CACHE_TTL_SECONDS).
Optional filters, combined with AND (invalid values return 400):
| Param | Description |
|---|---|
q |
Case-insensitive substring of the event name or venue (1–100 chars) |
category |
One of the Industry values, e.g. CONCERTS |
from |
ISO 8601; only events with startsAt >= from |
to |
ISO 8601; only events with startsAt <= to; must not precede from |
curl -s "$API/v1/events?page=1&limit=20"
curl -s "$API/v1/events?q=jazz&category=CONCERTS&from=2026-11-01T00:00:00Z&to=2026-11-30T23:59:59Z"{
"items": [
{
"id": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"name": "Launch Night",
"description": "A night of live music at The Fillmore.",
"imageUrl": "https://cdn.example.com/launch-night.jpg",
"timezone": "America/Los_Angeles",
"status": "PUBLISHED",
"chainEventId": "42",
"publishedTxHash": "abababababababababababababababababababababababababababababababab",
"ticketTypes": [{ "id": "4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e", "name": "General Admission", "price": "2500000", "quantityTotal": 500, "quantityIssued": 12, "isHidden": false }],
"organization": { "name": "Fillmore Live", "slug": "fillmore-live" }
}
],
"total": 1,
"page": 1,
"limit": 20
}(Other event columns omitted here for brevity; the response carries the full row.)
One event row with its organization row embedded. PUBLISHED events are
public; a DRAFT (or any non-published) event is visible only to members of
its owning organization — everyone else, signed in or not, gets 404 so an
unannounced event's existence isn't leaked. 404 when missing.
curl -s "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c"Members only. Paginated, newest first, soft-deleted events excluded.
| query | type | rules |
|---|---|---|
status |
string | optional, DRAFT, PUBLISHED or CANCELLED |
page |
integer | optional, 1-based, default 1, max 100000 |
limit |
integer | optional, default 20, 1 to 100 |
curl -s "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/events?status=DRAFT&page=1&limit=20" \
-H "Authorization: Bearer $TOKEN"{
"items": [{ "id": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c", "name": "Launch Night", "status": "DRAFT", "ticketTypes": [] }],
"total": 1,
"page": 1,
"limit": 20
}Members only. Counts of the organization's events per status, soft-deleted events excluded. Every status is returned, including zeros.
curl -s "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/events/summary" \
-H "Authorization: Bearer $TOKEN"{
"total": 4,
"byStatus": { "DRAFT": 3, "PUBLISHED": 1, "CANCELLED": 0 }
}Members only. Creates a DRAFT event.
| field | type | rules |
|---|---|---|
name |
string | required, 2 to 256 characters after trimming |
description |
string | required, 1 to 5000 characters after trimming |
imageUrl |
string or null | optional, absolute URL when provided |
category |
string | required, an Industry value |
venue |
string | required, 1 to 256 characters after trimming |
timezone |
string | required, valid IANA time-zone identifier |
startsAt |
date | required, not more than 60 seconds in the past |
endsAt |
date | optional |
maxResaleMultiplierBps |
integer | optional, 10000 to 50000, default 11000 (resale capped at 110% of face value) |
royaltyBps |
integer | optional, 0 to 2000, default 500 (5% of each resale to the organizer) |
curl -s -X POST "$API/v1/organizations/2c7e4a10-5b6d-4f8e-9a1b-3c4d5e6f7a8b/events" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Launch Night","description":"A night of live music at The Fillmore.","imageUrl":"https://cdn.example.com/launch-night.jpg","category":"CONCERTS","venue":"The Fillmore","timezone":"America/Los_Angeles","startsAt":"2026-12-01T20:00:00.000Z","endsAt":"2026-12-01T23:00:00.000Z","maxResaleMultiplierBps":11000,"royaltyBps":500}'Returns the event row.
Members only. 400 unless the event is still a DRAFT — once published the
sellable inventory is mirrored on-chain, so new ticket types would drift the
two apart.
| field | type | rules |
|---|---|---|
name |
string | required, 1 to 256 characters after trimming |
price |
string | required, digits only, no sign, no leading zeros, up to 39 digits |
quantityTotal |
integer | required, positive |
saleStartsAt |
date | optional |
saleEndsAt |
date | optional, must be after saleStartsAt when both are set |
isHidden |
boolean | optional, default false; hidden types are left out of GET /v1/events |
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/ticket-types" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"General Admission","price":"2500000","quantityTotal":500}'Returns the ticket type row.
Members only. Updates a ticket type's total inventory. The value may equal
quantityIssued, but cannot be lower; an attempted reduction below the
issued count returns 409.
| field | type | rules |
|---|---|---|
quantityTotal |
integer | required, positive and at least quantityIssued |
curl -s -X PATCH "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/ticket-types/4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"quantityTotal":750}'Members only. Build step: reserves a chainEventId and returns the
unsigned create_event envelope for the organization's Stellar account
to sign. 400 unless the event is a DRAFT that was never confirmed
on-chain and has no pending reservation, 409 until it has at least one
ticket type. A reservation stays pending for the envelope's time bound
(TRANSACTION_BUILD_TIMEOUT_SECONDS); if it was never confirmed, the next
call reuses the same chainEventId and returns a fresh envelope.
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/publish" -H "Authorization: Bearer $TOKEN"{ "unsignedXdr": "AAAAAgAA..." }Members only. Relays the signed envelope, reads the event back from the
contract to confirm the id and organizer match, then sets
status: PUBLISHED and publishedTxHash. 409 before anything is
submitted unless the event is still a DRAFT.
| field | type | rules |
|---|---|---|
signedXdr |
string | required |
txHash |
string | optional, 64 hex characters |
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/confirm-publish" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"signedXdr":"AAAAAgAA..."}'Returns the event row with status: "PUBLISHED", chainEventId: "42"
and publishedTxHash set.
Members only. Back to DRAFT. 400 unless PUBLISHED, 409 once any
ticket exists.
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/unpublish" -H "Authorization: Bearer $TOKEN"Members only. Soft delete and restore; both return the event row.
curl -s -X DELETE "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c" -H "Authorization: Bearer $TOKEN"
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/restore" -H "Authorization: Bearer $TOKEN"Members of the event's organization only. A gate is a named entrance that check-ins can be attributed to.
| field | type | rules |
|---|---|---|
name |
string | required, 1 to 256 characters after trimming |
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/gates" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Front Door"}'{ "id": "8c0e2a4b-6d1f-4c3e-9a5b-7d8e9f0a1b2d", "eventId": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c", "name": "Front Door", "createdAt": "2026-09-27T12:20:00.000Z" }Array of gate rows, oldest first.
curl -s "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/gates" -H "Authorization: Bearer $TOKEN"200 with an empty body. 404 when the gate is not on that event.
curl -s -X DELETE "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/gates/8c0e2a4b-6d1f-4c3e-9a5b-7d8e9f0a1b2d" -H "Authorization: Bearer $TOKEN"Create, list and revoke need membership in the event's organization. Preview is for any signed-in buyer.
| field | type | rules |
|---|---|---|
code |
string | required, 3 to 32 of A-Z, 0-9, _, -; unique per event |
discountType |
string | required, PERCENT or FIXED |
discountValue |
integer | required, at least 1; basis points (0 to 10000) for PERCENT, a smallest-unit amount for FIXED |
maxRedemptions |
integer | optional, at least 1 |
expiresAt |
string | optional, ISO-8601 |
isActive |
boolean | optional, default true |
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/promo-codes" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"code":"EARLYBIRD","discountType":"PERCENT","discountValue":2000,"maxRedemptions":100,"expiresAt":"2026-11-01T00:00:00.000Z"}'{
"id": "3a5c7e9b-1d2f-4e4a-8c6b-9d0e1f2a3b4c",
"eventId": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"code": "EARLYBIRD",
"discountType": "PERCENT",
"discountValue": 2000,
"maxRedemptions": 100,
"redemptionCount": 0,
"expiresAt": "2026-11-01T00:00:00.000Z",
"isActive": true,
"createdAt": "2026-09-27T12:25:00.000Z",
"updatedAt": "2026-09-27T12:25:00.000Z"
}Array of promo code rows, newest first.
curl -s "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/promo-codes" -H "Authorization: Bearer $TOKEN"Prices a code for the caller against a ticket type without redeeming
it. 400 with a reason when the code is unknown, inactive, expired,
exhausted or already used by the caller.
| field | type | rules |
|---|---|---|
ticketTypeId |
string | required, UUID |
code |
string | required, 3 to 32 characters (case-insensitive) |
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/promo-codes/preview" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"ticketTypeId":"4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e","code":"EARLYBIRD"}'{
"promoCode": { "id": "3a5c7e9b-1d2f-4e4a-8c6b-9d0e1f2a3b4c", "discountType": "PERCENT", "discountValue": 2000 },
"discountedPrice": "2000000",
"discountAmount": "500000"
}Deactivates the code (isActive: false) and returns the row.
curl -s -X DELETE "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/promo-codes/3a5c7e9b-1d2f-4e4a-8c6b-9d0e1f2a3b4c" -H "Authorization: Bearer $TOKEN"Members of the event's organization only. A device token lets a gate scanner confirm check-ins without a staff JWT.
| field | type | rules |
|---|---|---|
name |
string | required, 1 to 256 characters after trimming |
curl -s -X POST "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/scanner-devices" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Front Door Scanner"}'{
"device": {
"id": "0b2d4f6a-8c1e-4a3b-9d5f-6e7a8b9c0d2e",
"eventId": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"name": "Front Door Scanner",
"tokenHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"lastUsedAt": null,
"revokedAt": null,
"createdAt": "2026-09-27T12:30:00.000Z"
},
"token": "scn_4f1c...e2a9"
}token is shown once; only its hash is stored.
Array of device rows (without tokens), newest first.
curl -s "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/scanner-devices" -H "Authorization: Bearer $TOKEN"Sets revokedAt; the token stops authenticating immediately. Returns
the row.
curl -s -X PATCH "$API/v1/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/scanner-devices/0b2d4f6a-8c1e-4a3b-9d5f-6e7a8b9c0d2e/revoke" \
-H "Authorization: Bearer $TOKEN"Routes live under /v1/ticket-types/:ticketTypeId/waitlist. Position
is 1-based FIFO among WAITING entries. A waitlist entry row:
{
"id": "2f4a6c8e-0b1d-4c3e-9a5b-7d8e9f0a1b3e",
"ticketTypeId": "4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e",
"userId": "9d3a5c1e-7b2f-4e8a-9c6d-1f2e3a4b5c6d",
"status": "WAITING",
"offeredAt": null,
"offerExpiresAt": null,
"createdAt": "2026-09-27T12:35:00.000Z",
"updatedAt": "2026-09-27T12:35:00.000Z"
}Joins (or re-joins) the waitlist. 409 while the type still has
capacity or when the caller is already waiting.
curl -s -X POST "$API/v1/ticket-types/4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e/waitlist" -H "Authorization: Bearer $TOKEN"{ "entry": { "id": "2f4a6c8e-0b1d-4c3e-9a5b-7d8e9f0a1b3e", "status": "WAITING" }, "position": 3 }Leaves the waitlist; returns the entry with status: "CANCELLED".
404 when the caller is not WAITING.
curl -s -X DELETE "$API/v1/ticket-types/4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e/waitlist" -H "Authorization: Bearer $TOKEN"The caller's entry and position, same shape as join.
curl -s "$API/v1/ticket-types/4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e/waitlist/me" -H "Authorization: Bearer $TOKEN"Organizer view (members only): entries oldest first, each with
user: { id, name, email }.
curl -s "$API/v1/ticket-types/4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e/waitlist" -H "Authorization: Bearer $TOKEN"Members only. Moves the next count waiting entries to OFFERED with
an expiry window.
| field | type | rules |
|---|---|---|
count |
integer | required, 1 to 500 |
windowMs |
integer | optional, at least 60000; default 24 hours |
curl -s -X POST "$API/v1/ticket-types/4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e/waitlist/offer-next" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"count":10,"windowMs":86400000}'Returns the offered entries with status: "OFFERED", offeredAt and
offerExpiresAt set.
A ticket row, as returned by every ticket-producing route:
{
"id": "6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c",
"eventId": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"ticketTypeId": "4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e",
"ownerId": "9d3a5c1e-7b2f-4e8a-9c6d-1f2e3a4b5c6d",
"chainTicketId": "7",
"seat": "A-12",
"status": "VALID",
"qrSecret": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"issuedTxHash": "abababababababababababababababababababababababababababababababab",
"checkedInAt": null,
"checkInReason": null,
"checkedInGateId": null,
"createdAt": "2026-09-27T12:40:00.000Z",
"updatedAt": "2026-09-27T12:40:00.000Z"
}status is one of VALID, USED, REVOKED, RESALE. The flows behind
the build / confirm pairs are drawn in
ARCHITECTURE.md.
The caller's tickets, newest first, each with event and ticketType
embedded. status is optional and must be a TicketStatus value;
anything else is a 400.
curl -s "$API/v1/tickets/mine?status=VALID" -H "Authorization: Bearer $TOKEN"[
{
"id": "6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c",
"status": "VALID",
"seat": "A-12",
"chainTicketId": "7",
"event": { "id": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c", "name": "Launch Night", "startsAt": "2026-12-01T20:00:00.000Z" },
"ticketType": { "id": "4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e", "name": "General Admission", "price": "2500000" }
}
]Staff lookup by on-chain id (members of the ticket's organization). The
row comes with event (including organization) and ticketType.
404 for an unknown or malformed id.
curl -s "$API/v1/tickets/by-chain/7" -H "Authorization: Bearer $TOKEN"One ticket with its event (including organization) and ticketType, for
its owner or members of the event's organization. Anyone else gets 403;
an unknown id is 404. Staff do not receive the ticket's qrSecret.
curl -s "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c" -H "Authorization: Bearer $TOKEN"Owner only (403 for anyone else, 404 for an unknown id). A 512px PNG
(Content-Type: image/png, Cache-Control: no-store) encoding the ticket's
qrSecret, ready for a client to display.
curl -s "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/qr" -H "Authorization: Bearer $TOKEN" -o ticket.pngOwner only. A short-lived signed token to render in place of the static
qrSecret, so a screenshot of the QR code stops working once the token
expires. Fetch a fresh one before expiresAt (Cache-Control: no-store).
| Setting | Default | Meaning |
|---|---|---|
QR_TOKEN_TTL_SECONDS |
30 |
How long a token lasts |
QR_TOKEN_CLOCK_SKEW_SECONDS |
5 |
Extra grace after expiry, for scanners whose clock runs behind |
curl -s "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/qr-token" -H "Authorization: Bearer $TOKEN"{
"token": "v1.6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c.1790592030.q2Zk...base64url...Ag",
"expiresAt": "2026-10-17T12:00:30.000Z",
"ttlSeconds": 30
}Gate verification (members only, rate limited per SCAN_RATE_LIMIT_*,
429 when exceeded). :qrSecret is either the ticket's static qrSecret
or a rotating token from GET /v1/tickets/:ticketId/qr-token; an expired
token (past its expiry plus QR_TOKEN_CLOCK_SKEW_SECONDS) is rejected with
400, and a forged one is 404, like an unknown secret. Reads the ticket from the contract, reconciles the
cached status, and falls back to the cached row with stale: true when
Soroban RPC is unreachable. ownerMismatch is true when the recorded
owner's wallet is not onChainOwner; the owner is then reconciled to the
account holding that wallet, if there is one, and ownerName names them.
It is false when stale is true, since there is nothing to compare.
curl -s "$API/v1/tickets/verify/c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f" -H "Authorization: Bearer $TOKEN"{
"ticketId": "6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c",
"eventName": "Launch Night",
"tier": "General Admission",
"seat": "A-12",
"ownerName": "Ada Lovelace",
"status": "VALID",
"onChainOwner": "GBAHZWO3UI3GAHPQCPSW6IR5N7HJ4UBRZNAFMSYB6DAKVNHQDOZIV2YJ",
"ownerMismatch": false,
"stale": false
}Ed25519 public keys (PEM) keyed by key id, for verifying offline tokens at a gate with no connectivity. See OFFLINE_VERIFICATION.md.
curl -s "$API/v1/tickets/offline-public-keys" -H "Authorization: Bearer $TOKEN"{ "2026-09": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n" }Members only. A signed snapshot of the ticket valid for 12 hours.
curl -s "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/offline-token" -H "Authorization: Bearer $TOKEN"{
"payload": { "ticketId": "6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c", "chainTicketId": "7", "eventId": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c", "status": "VALID", "exp": 1790592000 },
"kid": "2026-09",
"signature": "q2Zk...base64url...Ag"
}Organizer-authorized issuance for a sale settled off-chain. Members of the event's organization only; the organization's Stellar account signs.
| field | type | rules |
|---|---|---|
ticketTypeId |
string | required, UUID |
toUserId |
string | required, UUID of the recipient account |
toPublicKey |
string | required, must equal the recipient's connected wallet |
seat |
string | optional, 1 to 64 characters of letters, digits, spaces, -, /, .; defaults to unassigned |
curl -s -X POST "$API/v1/tickets/issue" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'Idempotency-Key: issue-7f3a' \
-d '{"ticketTypeId":"4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e","toUserId":"5e2b9f0c-1a3d-4b7e-8f9a-0c1d2e3f4a5b","toPublicKey":"GD6ROJBYLKQMOW3E7N4M2YBPUHMZD7PL65VRHRMO24BOVSBV5H3BQRSL","seat":"A-12"}'{ "unsignedXdr": "AAAAAgAA..." }400 when the event is unpublished, sales are closed, or the wallet
does not match; 409 (TICKET_TYPE_SOLD_OUT) when the type is full.
Same four fields plus signedXdr and optional txHash.
curl -s -X POST "$API/v1/tickets/confirm-issue" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"ticketTypeId":"4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e","toUserId":"5e2b9f0c-1a3d-4b7e-8f9a-0c1d2e3f4a5b","toPublicKey":"GD6ROJBYLKQMOW3E7N4M2YBPUHMZD7PL65VRHRMO24BOVSBV5H3BQRSL","seat":"A-12","signedXdr":"AAAAAgAA..."}'Returns the new ticket row. 409 when the seat is already issued for
that event.
The buyer signs with their own wallet.
| field | type | rules |
|---|---|---|
ticketTypeId |
string | required, UUID |
seat |
string | optional, same rules as issue |
promoCode |
string | optional, 3 to 32 characters; priced at build time, redeemed after confirm |
curl -s -X POST "$API/v1/tickets/purchase" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'Idempotency-Key: buy-91c2' \
-d '{"ticketTypeId":"4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e","promoCode":"EARLYBIRD"}'{ "unsignedXdr": "AAAAAgAA..." }| field | type | rules |
|---|---|---|
ticketTypeId |
string | required, UUID |
seat |
string | optional |
promoCode |
string | optional, 3 to 32 characters |
signedXdr |
string | required |
txHash |
string | optional, 64 hex characters |
curl -s -X POST "$API/v1/tickets/confirm-purchase" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"ticketTypeId":"4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e","promoCode":"EARLYBIRD","signedXdr":"AAAAAgAA..."}'Returns the new ticket row (ownerId is the caller) and emails a
receipt.
The current owner signs.
| field | type | rules |
|---|---|---|
toUserId |
string | required, UUID |
toPublicKey |
string | required, must equal the recipient's connected wallet |
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/transfer" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"toUserId":"5e2b9f0c-1a3d-4b7e-8f9a-0c1d2e3f4a5b","toPublicKey":"GD6ROJBYLKQMOW3E7N4M2YBPUHMZD7PL65VRHRMO24BOVSBV5H3BQRSL"}'{ "unsignedXdr": "AAAAAgAA..." }403 when the caller does not own the ticket.
Same two fields plus signedXdr and optional txHash.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/confirm-transfer" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"toUserId":"5e2b9f0c-1a3d-4b7e-8f9a-0c1d2e3f4a5b","toPublicKey":"GD6ROJBYLKQMOW3E7N4M2YBPUHMZD7PL65VRHRMO24BOVSBV5H3BQRSL","signedXdr":"AAAAAgAA..."}'Returns the ticket row with the new ownerId and status: "VALID".
Members of the event's organization; the organization's Stellar account signs.
No body.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/check-in" -H "Authorization: Bearer $TOKEN"{ "unsignedXdr": "AAAAAgAA..." }| field | type | rules |
|---|---|---|
signedXdr |
string | required |
txHash |
string | optional, 64 hex characters |
gateId |
string | optional, UUID of a gate on this event |
reason |
string | optional, up to 512 characters, for manual overrides |
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/confirm-check-in" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"signedXdr":"AAAAAgAA...","gateId":"8c0e2a4b-6d1f-4c3e-9a5b-7d8e9f0a1b2d"}'Returns the ticket row with status: "USED", checkedInAt,
checkedInGateId and checkInReason.
Same body as confirm-check-in, authenticated with a scanner device
token instead of a JWT: either Authorization: Bearer <token> or
x-device-token: <token>. 401 when the token is unknown, revoked, or
registered for a different event than the ticket's.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/scanner-check-in" \
-H "x-device-token: scn_4f1c...e2a9" -H 'Content-Type: application/json' \
-d '{"signedXdr":"AAAAAgAA...","gateId":"8c0e2a4b-6d1f-4c3e-9a5b-7d8e9f0a1b2d"}'Returns the ticket row with status: "USED".
Members of the event's organization.
No body. Returns { "unsignedXdr": "AAAAAgAA..." }.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/revoke" -H "Authorization: Bearer $TOKEN"Body is ConfirmSignedTxDto (signedXdr, optional txHash). Returns
the ticket row with status: "REVOKED" and writes an audit entry. An
ACTIVE resale listing for the ticket is set to CANCELLED in the same
transaction, so it leaves GET /v1/tickets/resale.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/confirm-revoke" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"signedXdr":"AAAAAgAA..."}'Off-chain bulk revoke for fraud response.
| field | type | rules |
|---|---|---|
ticketIds |
string[] | required, up to 100 ids of at most 100 characters each, all on this event |
curl -s -X POST "$API/v1/tickets/events/7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c/revoke-batch" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"ticketIds":["6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c","0d1e2f3a-4b5c-4d6e-8f7a-8b9c0d1e2f3a"]}'{ "count": 2 }400 when any id is missing from the event or more than 100 are sent.
A resale listing row:
{
"id": "1e3c5a7b-9d0f-4b2c-8e4a-6f7a8b9c0d1f",
"ticketId": "6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c",
"sellerId": "9d3a5c1e-7b2f-4e8a-9c6d-1f2e3a4b5c6d",
"price": "2750000",
"status": "ACTIVE",
"txHash": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
"expiresAt": "2026-11-30T00:00:00.000Z",
"createdAt": "2026-09-27T13:00:00.000Z",
"updatedAt": "2026-09-27T13:00:00.000Z"
}The asking price may not exceed
floor(ticketType.price * event.maxResaleMultiplierBps / 10000)
(400 otherwise), and a seller may hold at most
MAX_ACTIVE_RESALE_LISTINGS_PER_USER active listings (409).
Owner only.
| field | type | rules |
|---|---|---|
price |
string | required, digits only, up to 39 digits |
expiresAt |
string | optional, ISO-8601 (stored at confirm time) |
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/list-resale" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"price":"2750000"}'{ "unsignedXdr": "AAAAAgAA..." }| field | type | rules |
|---|---|---|
price |
string | required, digits only, up to 39 digits |
expiresAt |
string | optional, ISO-8601 |
signedXdr |
string | required |
txHash |
string | optional, 64 hex characters |
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/confirm-list-resale" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"price":"2750000","expiresAt":"2026-11-30T00:00:00.000Z","signedXdr":"AAAAAgAA..."}'Returns the new listing row; the ticket moves to status: "RESALE".
409 when the ticket already has an active listing.
Marketplace of ACTIVE listings, newest first, cursor paginated.
| query | type | rules |
|---|---|---|
cursor |
string | optional, the previous response's nextCursor |
limit |
integer | optional, default 20, 1 to 100 |
curl -s "$API/v1/tickets/resale?limit=20" -H "Authorization: Bearer $TOKEN"{
"items": [
{
"id": "1e3c5a7b-9d0f-4b2c-8e4a-6f7a8b9c0d1f",
"price": "2750000",
"status": "ACTIVE",
"expiresAt": "2026-11-30T00:00:00.000Z",
"ticket": {
"id": "6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c",
"seat": "A-12",
"event": { "id": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c", "name": "Launch Night", "royaltyBps": 500 },
"ticketType": { "id": "4b8d2f6a-0c1e-4a3b-9d5f-6e7a8b9c0d1e", "name": "General Admission", "price": "2500000" }
},
"seller": { "name": "Ada Lovelace" },
"royaltyFee": "137500",
"sellerProceeds": "2612500"
}
],
"nextCursor": "MjAyNi0wOS0yN1QxMzowMDowMC4wMDBafDFlM2M1YTdiLTlkMGYtNGIyYy04ZTRhLTZmN2E4YjljMGQxZg",
"limit": 20
}royaltyFee = floor(price * royaltyBps / 10000) and
sellerProceeds = price - royaltyFee. nextCursor is null on the
last page; a malformed cursor is a 400.
Every asking price the listing has had, oldest first.
curl -s "$API/v1/tickets/resale/1e3c5a7b-9d0f-4b2c-8e4a-6f7a8b9c0d1f/price-history" -H "Authorization: Bearer $TOKEN"[
{ "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "resaleListingId": "1e3c5a7b-9d0f-4b2c-8e4a-6f7a8b9c0d1f", "price": "2750000", "createdAt": "2026-09-27T13:00:00.000Z" },
{ "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "resaleListingId": "1e3c5a7b-9d0f-4b2c-8e4a-6f7a8b9c0d1f", "price": "2600000", "createdAt": "2026-09-28T09:00:00.000Z" }
]Seller only, listing must be ACTIVE. Off-chain: records the new
asking price and a history row.
| field | type | rules |
|---|---|---|
price |
string | required, digits only, within the event cap |
curl -s -X PATCH "$API/v1/tickets/resale/1e3c5a7b-9d0f-4b2c-8e4a-6f7a8b9c0d1f/price" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"price":"2600000"}'Returns the listing row with the new price.
Any signed-in user with a wallet. 400 (LISTING_INACTIVE) unless the
ticket is in RESALE.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/buy-resale" -H "Authorization: Bearer $TOKEN"{ "unsignedXdr": "AAAAAgAA..." }Body is ConfirmSignedTxDto. Marks the listing SOLD and returns the
ticket row with the buyer as ownerId and status: "VALID".
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/confirm-buy-resale" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"signedXdr":"AAAAAgAA..."}'Owner only. Returns { "unsignedXdr": "AAAAAgAA..." }.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/cancel-resale" -H "Authorization: Bearer $TOKEN"Body is ConfirmSignedTxDto. Marks the listing CANCELLED and the
ticket VALID. Responds 201 with an empty body.
curl -s -X POST "$API/v1/tickets/6f0a2c4e-8b1d-4f3a-9e5c-7d8e9f0a1b2c/confirm-cancel-resale" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"signedXdr":"AAAAAgAA..."}'Sweeps ACTIVE listings whose expiresAt has passed: listing to
CANCELLED, ticket back to VALID. The scheduler runs the same job;
see RESALE_EXPIRY.md.
curl -s -X POST "$API/v1/tickets/resale/cancel-expired" -H "Authorization: Bearer $TOKEN"{ "cancelledCount": 3 }Public. State of the breaker that wraps every Soroban RPC call
(CLOSED, OPEN or HALF_OPEN).
curl -s "$API/v1/stellar/circuit-breaker/metrics"{ "state": "CLOSED", "consecutiveFailures": 0, "totalSuccesses": 1240, "totalFailures": 2, "rejectedCalls": 0 }