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.
The examples assume:
API=http://localhost:3000
TOKEN=<accessToken from /v1/auth/login>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.
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"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: 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).
curl -s "$API/v1/events"[
{
"id": "7a1f3c5e-9b2d-4e6f-8a0c-1d2e3f4a5b6c",
"name": "Launch Night",
"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" }
}
](Other event columns omitted here for brevity; the response carries the full row.)
Public. One event row with its organization row embedded. 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. Creates a DRAFT event.
| field | type | rules |
|---|---|---|
name |
string | required, 2 to 256 characters after trimming |
category |
string | required, an Industry value |
venue |
string | required, 1 to 256 characters after trimming |
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","category":"CONCERTS","venue":"The Fillmore","startsAt":"2026-12-01T20:00:00.000Z","endsAt":"2026-12-01T23:00:00.000Z","maxResaleMultiplierBps":11000,"royaltyBps":500}'Returns the event row.
Members only.
| 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. 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 without a chain id, 409
until it has at least one ticket type.
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.
| 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"Gate verification (members only, rate limited per SCAN_RATE_LIMIT_*,
429 when exceeded). 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.
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",
"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.
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 }