Pass

TerraPass — events, doorlists, tiers, shifts, the till and the fan surface.

56 endpoints

POST /v3/auth/fan/claim #
public login

send a code to the contact on the ticket.

ALWAYS answers the same way, whether or not the contact matches anybody. A response that differed would turn this endpoint into a directory of who holds a ticket to tonight's event, which is exactly the kind of thing an event's guest list must not leak (OWASP: no user enumeration on an unauthenticated surface).

Request body

FieldTypeConstraints
phonestring | nulloptional
emailstring | nulloptionalmax length 160

Response

Returns { "data": PassFanClaim }.

FieldTypeConstraints
sentbooleanrequired
challenge_idstring | nullrequired
expires_ininteger | nullrequired
POST /v3/auth/fan/session #
public 10,1

code → a `pass_fan` session.

Request body

FieldTypeConstraints
challenge_idstring (uuid)required
otpstringrequiredmin length 6 · max length 6

Response

Returns { "data": AuthTokens }.

FieldTypeConstraints
access_tokenstringrequired
token_typestringrequired
expires_inintegerrequired
refresh_tokenstringrequired
POST /v3/pass/band/{serial}/organizer #
bearer pass_app pass_web access.band_manage device credential

`POST /v3/pass/band/{serial}/organizer` — hand a tag from the platform pool to a venue.

THE MISSING STEP between "the box is registered" and "the desk can issue one". Stock is registered by a platform operator, whose session carries NO tenant, so the row lands with tenant_id = NULL — the pool. Issuing reads BandService::find, which is scoped where tenant_id = $ctx->tenantId, so a desk holding an organizer's session can never see a pooled tag: it answers unknown_band and the steward is told the wristband is not registered, which is exactly what a genuinely unregistered one says.

Found live on 2026-08-25: three sample tags Eric had added sat in the pool, and the only mover was /v3/portal/band/{serial}/school — a surface an event console cannot reach. The tags had to be moved by hand with an admin token.

PLATFORM ONLY, on the same fence as {@see index}: the pool is Terra's inventory, and a venue that could pull from it could hand itself credentials nobody paid for. That is the whole reason the whitelist exists.

Path parameters

NameTypeNotes
serialstring

Request body

FieldTypeConstraints
organizer_idstring (uuid)required

Response

Returns { "data": BandMovedToOrganizer }.

FieldTypeConstraints
serial_numberstringrequired
statusstringrequired
organizer_idstring (uuid)required
GET /v3/pass/catalogue #
bearer pass_app pass_web device credential

`zone` is the OUTLET the handset is working, and it narrows the list to the vendors who trade there (Cloud9 7).

Read from the query rather than resolved from the device row, because a handset is moved between stations during a night — the till's own picker already knows where the steward is standing, and pay resolves the settlement zone from the device independently. Omitted, the whole tenant's catalogue comes back, which is what every caller got before and what a single-bar venue still wants.

Query parameters

NameTypeConstraints
zonestring | nulloptional

Response

Returns { "data": PassCatalogue }.

FieldTypeConstraints
categoriesobject[]required
itemsobject[]required
POST /v3/pass/catalogue/parse #
bearer pass_app pass_web settings.manage device credential

Request body

FieldTypeConstraints
textstringrequiredmin length 3 · max length 12000

Response

Returns { "data": VendorListProposal }.

FieldTypeConstraints
itemsobject[]required
skippedintegerrequired
POST /v3/pass/device/register #
bearer pass_app device credential

Request body

FieldTypeConstraints
serial_numberstringrequiredmax length 64
namestring | nulloptionalmax length 120
zone_idstring | nulloptional
modelstring | nulloptionalmax length 120
firmwarestring | nulloptionalmax length 64

Response

Returns { "data": PassDevice }.

FieldTypeConstraints
idstring (uuid)required
serial_numberstringrequired
namestringrequired
zone_idstring | nullrequired
statusstringrequired
registeredbooleanrequired
GET /v3/pass/devices #
bearer pass_web device credential

the organizer's fleet, for the console.

Rides the surface-neutral {@see WebDeviceService} (the same read the school console uses) and re-projects at THIS surface: pass has no schools, so the row carries the device, its zone and its derived presence — vocabulary at the surface, one query underneath.

Query parameters

NameTypeConstraints
statusstring | nulloptionalmax length 40
kindstring | nulloptionalmax length 40
zone_idstring | nulloptional
qstring | nulloptionalmax length 120

Response

Returns { "data": [PassDeviceRow] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
namestringrequired
serial_numberstringrequired
kindDeviceKindrequired
statusDeviceStatusrequired
zoneobject | nullrequired
zone.idstring (uuid)required
zone.namestringrequired
battery_pctinteger | nullrequired
signal_strengthinteger | nullrequired
sim_presentboolean | nullrequired
firmwarestring | nullrequired
presencestringrequired
last_seen_atstring | nullrequired
row_versionintegerrequired
GET /v3/pass/events #
bearer pass_app pass_web device credential

keyset page, newest first.

Response

Returns { "data": [PassEvent] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
organizer_idstring (uuid)required
namestringrequired
slugstringrequired
zone_idstring | nullrequired
statusstringrequired
starts_atstring | nullrequired
ends_atstring | nullrequired
timezonestringrequired
currency_codestringrequired
balance_carriesbooleanrequired
credential_grace_minutesintegerrequired
settingsobjectrequired
created_atstringrequired
row_versionintegerrequired
POST /v3/pass/events #
bearer pass_web settings.manage device credential

mints the event AND its root zone together.

Request body

FieldTypeConstraints
namestringrequiredmax length 160
slugstring | nulloptionalmax length 160
starts_atstring | nulloptional
ends_atstring | nulloptional
timezonestring | nulloptional
statusstring | nulloptionaldraft published live settling closed cancelled
currency_codestring | nulloptionalmin length 3 · max length 3
balance_carriesboolean | nulloptional
credential_grace_minutesinteger | nulloptionalmin 0 · max 10080
settingsobject | nulloptional

Response

Returns { "data": PassEvent }.

FieldTypeConstraints
idstring (uuid)required
organizer_idstring (uuid)required
namestringrequired
slugstringrequired
zone_idstring | nullrequired
statusstringrequired
starts_atstring | nullrequired
ends_atstring | nullrequired
timezonestringrequired
currency_codestringrequired
balance_carriesbooleanrequired
credential_grace_minutesintegerrequired
settingsobjectrequired
created_atstringrequired
row_versionintegerrequired
GET /v3/pass/events/{eventId}/cash-desk #
bearer pass_app pass_web device credential

What the cash desk took on this event.

Beside the shifts and not inside them on purpose: a desk top-up is not attributable to a till. Nobody's drawer is short because of it, and folding it into a steward's ledgered figure would make a shift answer for money it never handled.

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": PassCashDesk }.

FieldTypeConstraints
taken_minorintegerrequired
topupsintegerrequired
refunds_minorintegerrequired
currencystringrequired
GET /v3/pass/events/{eventId}/guests #
bearer pass_app pass_web device credential

keyset page, newest first.

Path parameters

NameTypeNotes
eventIdstring

Query parameters

NameTypeConstraints
searchstring | nulloptionalmax length 120

Response

Returns { "data": [PassGuest] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
account_idstring (uuid)required
namestringrequired
phonestring | nullrequired
emailstring | nullrequired
ticket_refstringrequired
ticket_typestring | nullrequired
tier_idstring | nullrequired
tier_namestring | nullrequired
statusstringrequiredissued blocked refunded
sourcestringrequired
holder_confirmedbooleanrequired
holder_confirmed_atstring | nullrequired
band_serialstring | nullrequired
band_statusstring | nullrequired
checked_inbooleanrequired
walletanyrequired
notesstring | nullrequired
created_atstringrequired
row_versionintegerrequired
GET /v3/pass/events/{eventId}/guests/{id} #
bearer pass_app pass_web device credential

GET /v3/pass/events/{eventId}/guests/{id}

Path parameters

NameTypeNotes
eventIdstring
idstring

Response

Returns { "data": PassGuest }.

FieldTypeConstraints
idstring (uuid)required
account_idstring (uuid)required
namestringrequired
phonestring | nullrequired
emailstring | nullrequired
ticket_refstringrequired
ticket_typestring | nullrequired
tier_idstring | nullrequired
tier_namestring | nullrequired
statusstringrequiredissued blocked refunded
sourcestringrequired
holder_confirmedbooleanrequired
holder_confirmed_atstring | nullrequired
band_serialstring | nullrequired
band_statusstring | nullrequired
checked_inbooleanrequired
walletanyrequired
notesstring | nullrequired
created_atstringrequired
row_versionintegerrequired
GET /v3/pass/events/{eventId}/history #
bearer pass_app pass_web device credential

GET /v3/pass/events/{eventId}/history?days=1&device=TG-0042

Path parameters

NameTypeNotes
eventIdstring

Query parameters

NameTypeConstraints
daysinteger | nulloptionalmin 0 · max 365
devicestring | nulloptionalmax length 64
zonestring | nulloptional

Response

Returns { "data": PassHistory }.

FieldTypeConstraints
salesobject[]required
scansobject[]required
GET /v3/pass/events/{eventId}/overview #
bearer pass_app pass_web device credential

the console's headline tiles.

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": PassEventOverview }.

FieldTypeConstraints
tickets_totalintegerrequired
tickets_bandedintegerrequired
scans_allowedintegerrequired
scans_deniedintegerrequired
sales_countintegerrequired
sales_minorintegerrequired
currencystringrequired
GET /v3/pass/events/{eventId}/report #
bearer pass_app pass_web device credential

GET /v3/pass/events/{eventId}/report?days=1&device=TG-0042

Path parameters

NameTypeNotes
eventIdstring

Query parameters

NameTypeConstraints
daysinteger | nulloptionalmin 0 · max 365
devicestring | nulloptionalmax length 64
zonestring | nulloptional

Response

Returns { "data": PassReport }.

FieldTypeConstraints
currencystringrequired
net_minorintegerrequired
sales_countintegerrequired
items_soldintegerrequired
bucketsobject[]required
top_sellersobject[]required
GET /v3/pass/events/{eventId}/settlements #
bearer pass_web device credential

what each outlet is owed (T-7).

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": PassSettlements }.

FieldTypeConstraints
currencystringrequired
total_minorintegerrequired
outletsobject[]required
GET /v3/pass/events/{eventId}/shift #
bearer pass_app device credential

The caller's own running shift on this event.

404 when there is none. That is the handset's cue to show "open a shift" rather than a zeroed till, and the two are different states — a till showing Sh 0.00 with no shift open takes money nothing will account for.

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": PassShift }.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
zone_idstring | nullrequired
user_idstring (uuid)required
device_serialstring | nullrequired
statusstringrequired
opened_atstringrequired
closed_atstring | nullrequired
takings_minorintegerrequired
salesintegerrequired
ledgered_takings_minorintegerrequired
ledgered_salesintegerrequired
entries_grantedintegerrequired
entries_deniedintegerrequired
by_methodobject[]required
POST /v3/pass/events/{eventId}/shift #
bearer pass_app device credential

Path parameters

NameTypeNotes
eventIdstring

Request body

FieldTypeConstraints
zone_idstring | nulloptional
device_serialstring | nulloptionalmax length 120

Response

Returns { "data": PassShift }.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
zone_idstring | nullrequired
user_idstring (uuid)required
device_serialstring | nullrequired
statusstringrequired
opened_atstringrequired
closed_atstring | nullrequired
takings_minorintegerrequired
salesintegerrequired
ledgered_takings_minorintegerrequired
ledgered_salesintegerrequired
entries_grantedintegerrequired
entries_deniedintegerrequired
by_methodobject[]required
GET /v3/pass/events/{eventId}/shifts #
bearer pass_app pass_web device credential

Every shift on an event — the supervisor's cash-up view.

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": [PassShift] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
zone_idstring | nullrequired
user_idstring (uuid)required
device_serialstring | nullrequired
statusstringrequired
opened_atstringrequired
closed_atstring | nullrequired
takings_minorintegerrequired
salesintegerrequired
ledgered_takings_minorintegerrequired
ledgered_salesintegerrequired
entries_grantedintegerrequired
entries_deniedintegerrequired
by_methodobject[]required
POST /v3/pass/events/{eventId}/ticket/scan #
bearer pass_app device credential

POST /v3/pass/events/{eventId}/ticket/scan

Path parameters

NameTypeNotes
eventIdstring

Request body

FieldTypeConstraints
payloadstringrequiredmin length 8 · max length 4096

Response

Returns { "data": PassTicketScan }.

FieldTypeConstraints
verdictstringrequiredok ok_unverified already_used invalid unknown not_configured
ticketanyrequired
redeemed_atstring | nullrequired
POST /v3/pass/events/{eventId}/tickets #
bearer pass_web device credential

a comp, crew, or a door sale.

Path parameters

NameTypeNotes
eventIdstring

Request body

FieldTypeConstraints
first_namestringrequiredmax length 80
last_namestring | nulloptionalmax length 80
phonestring | nulloptional
emailstring | nulloptionalmax length 160
ticket_refstring | nulloptionalmax length 80
external_refstring | nulloptionalmax length 120
ticket_typestring | nulloptionalmax length 60
tier_idstring | nulloptional
notesstring | nulloptionalmax length 500

Response

Returns { "data": PassGuest }.

FieldTypeConstraints
idstring (uuid)required
account_idstring (uuid)required
namestringrequired
phonestring | nullrequired
emailstring | nullrequired
ticket_refstringrequired
ticket_typestring | nullrequired
tier_idstring | nullrequired
tier_namestring | nullrequired
statusstringrequiredissued blocked refunded
sourcestringrequired
holder_confirmedbooleanrequired
holder_confirmed_atstring | nullrequired
band_serialstring | nullrequired
band_statusstring | nullrequired
checked_inbooleanrequired
walletanyrequired
notesstring | nullrequired
created_atstringrequired
row_versionintegerrequired
DELETE /v3/pass/events/{eventId}/tickets/{id} #
bearer pass_web device credential

off the doorlist, kept for the record.

Path parameters

NameTypeNotes
eventIdstring
idstring

Response

Returns { "data": Deleted }.

FieldTypeConstraints
deletedbooleanrequired
PATCH /v3/pass/events/{eventId}/tickets/{id} #
bearer pass_web device credential

type, tier, status, notes.

Path parameters

NameTypeNotes
eventIdstring
idstring

Request body

FieldTypeConstraints
ticket_typestring | nulloptionalmax length 60
tier_idstring | nulloptional
statusstringoptionalissued blocked refunded
notesstring | nulloptionalmax length 500
update_maskstring[]requiredmin items 1
expected_row_versionintegerrequiredmin 1

Response

Returns { "data": PassGuest }.

FieldTypeConstraints
idstring (uuid)required
account_idstring (uuid)required
namestringrequired
phonestring | nullrequired
emailstring | nullrequired
ticket_refstringrequired
ticket_typestring | nullrequired
tier_idstring | nullrequired
tier_namestring | nullrequired
statusstringrequiredissued blocked refunded
sourcestringrequired
holder_confirmedbooleanrequired
holder_confirmed_atstring | nullrequired
band_serialstring | nullrequired
band_statusstring | nullrequired
checked_inbooleanrequired
walletanyrequired
notesstring | nullrequired
created_atstringrequired
row_versionintegerrequired
GET /v3/pass/events/{eventId}/tickets/{id}/holder #
bearer pass_web device credential

the chain of custody.

What a desk reads when somebody is refused at a rope. The ticket row only ever shows the LATEST holder, so "who had this before, and when did it move" cannot be answered from it.

CURSOR-PAGED like every other list here, and not because a chain of custody is long — most are one move. It is paged because the contract has two answer shapes, data and dataList, and this is a list: declaring it as anything else put an untyped bag on the wire, which is what every SDK generated for it until now (Record<string, unknown>). A caller that never sends a cursor still gets the whole history in one read.

Path parameters

NameTypeNotes
eventIdstring
idstring

Response

Returns { "data": [PassTicketHolderMove] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
kindstringrequiredclaim transfer correction
channelstring | nullrequired
notestring | nullrequired
fromanyrequired
toPassTicketHolderPartyrequired
atstringrequired
POST /v3/pass/events/{eventId}/tickets/{id}/holder #
bearer pass_web device credential

who is actually coming (0148).

A desk doing what a guest could not: claiming an open seat for somebody standing in front of them with no smartphone, recording a resale, or fixing a name. The self-service channels reach the same {@see TicketService::changeHolder}, so the history reads identically whichever door the change came through — only channel differs, which is exactly the fact a dispute wants.

actor_user_id is stamped from the token and never from the body: "on whose authority" is not something a caller gets to assert.

Path parameters

NameTypeNotes
eventIdstring
idstring

Request body

FieldTypeConstraints
kindstringrequiredclaim transfer correction
first_namestringrequiredmax length 80
last_namestring | nulloptionalmax length 80
phonestring | nulloptional
emailstring | nulloptionalmax length 160
notestring | nulloptionalmax length 500

Response

Returns { "data": PassGuest }.

FieldTypeConstraints
idstring (uuid)required
account_idstring (uuid)required
namestringrequired
phonestring | nullrequired
emailstring | nullrequired
ticket_refstringrequired
ticket_typestring | nullrequired
tier_idstring | nullrequired
tier_namestring | nullrequired
statusstringrequiredissued blocked refunded
sourcestringrequired
holder_confirmedbooleanrequired
holder_confirmed_atstring | nullrequired
band_serialstring | nullrequired
band_statusstring | nullrequired
checked_inbooleanrequired
walletanyrequired
notesstring | nullrequired
created_atstringrequired
row_versionintegerrequired
GET /v3/pass/events/{eventId}/tiers #
bearer pass_app pass_web device credential

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": [PassTier] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
namestringrequired
slugstringrequired
positionintegerrequired
colourstring | nullrequired
zone_idsstring (uuid)[]required
created_atstringrequired
updated_atstringrequired
row_versionintegerrequired
POST /v3/pass/events/{eventId}/tiers #
bearer pass_web settings.manage device credential

Path parameters

NameTypeNotes
eventIdstring

Request body

FieldTypeConstraints
namestringrequiredmax length 120
slugstring | nulloptionalmax length 120
zone_idsstring (uuid)[]requiredmin items 1
positioninteger | nulloptionalmin 0 · max 999
colourstring | nulloptionalmax length 32

Response

Returns { "data": PassTier }.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
namestringrequired
slugstringrequired
positionintegerrequired
colourstring | nullrequired
zone_idsstring (uuid)[]required
created_atstringrequired
updated_atstringrequired
row_versionintegerrequired
GET /v3/pass/events/{eventId}/zones #
bearer pass_app pass_web device credential

Path parameters

NameTypeNotes
eventIdstring

Response

Returns { "data": [PassZone] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
namestringrequired
kindZoneKindrequired
parent_zone_idstring | nullrequired
POST /v3/pass/events/{eventId}/zones #
bearer pass_web settings.manage device credential

make the event operable.

create mints the root and nothing else, and until now the only zone write in the codebase was on the legacy WaaS surface — so an organiser could build an event in their own console and then had nowhere to put a gate or a bar.

Path parameters

NameTypeNotes
eventIdstring

Request body

FieldTypeConstraints
namestringrequiredmax length 120
kindstringrequiredgate outlet office other
parent_zone_idstring | nulloptional

Response

Returns { "data": PassZone }.

FieldTypeConstraints
idstring (uuid)required
namestringrequired
kindZoneKindrequired
parent_zone_idstring | nullrequired
DELETE /v3/pass/events/{id} #
bearer pass_web settings.manage device credential

Path parameters

NameTypeNotes
idstring

Response

Returns { "data": … }; this endpoint's payload is not yet pinned to a named shape.

GET /v3/pass/events/{id} #
bearer pass_app pass_web device credential

Path parameters

NameTypeNotes
idstring

Response

Returns { "data": PassEvent }.

FieldTypeConstraints
idstring (uuid)required
organizer_idstring (uuid)required
namestringrequired
slugstringrequired
zone_idstring | nullrequired
statusstringrequired
starts_atstring | nullrequired
ends_atstring | nullrequired
timezonestringrequired
currency_codestringrequired
balance_carriesbooleanrequired
credential_grace_minutesintegerrequired
settingsobjectrequired
created_atstringrequired
row_versionintegerrequired
PATCH /v3/pass/events/{id} #
bearer pass_web settings.manage device credential

Path parameters

NameTypeNotes
idstring

Request body

FieldTypeConstraints
namestringoptionalmax length 160
slugstring | nulloptionalmax length 160
starts_atstring | nulloptional
ends_atstring | nulloptional
timezonestring | nulloptional
statusstring | nulloptionaldraft published live settling closed cancelled
currency_codestring | nulloptionalmin length 3 · max length 3
balance_carriesboolean | nulloptional
credential_grace_minutesinteger | nulloptionalmin 0 · max 10080
settingsobject | nulloptional
update_maskstring[]requiredmin items 1
expected_row_versionintegerrequiredmin 1

Response

Returns { "data": PassEvent }.

FieldTypeConstraints
idstring (uuid)required
organizer_idstring (uuid)required
namestringrequired
slugstringrequired
zone_idstring | nullrequired
statusstringrequired
starts_atstring | nullrequired
ends_atstring | nullrequired
timezonestringrequired
currency_codestringrequired
balance_carriesbooleanrequired
credential_grace_minutesintegerrequired
settingsobjectrequired
created_atstringrequired
row_versionintegerrequired
GET /v3/pass/me #
bearer pass_fan credential

the fan's first screen, in one call.

Profile, wallet and the first page of tickets together: a fan opens this on mobile data outside a venue, and the same one-request doctrine the steward bootstrap follows applies with more force to somebody on a phone in a queue.

Response

Returns { "data": PassFanHome }.

FieldTypeConstraints
accountobjectrequired
account.idstring (uuid)required
account.namestringrequired
account.phonestring | nullrequired
account.emailstring | nullrequired
walletanyrequired
ticketsPassFanTicket[]required
GET /v3/pass/me/balance #
bearer pass_fan credential

what they can spend at the bar right now.

Response

Returns { "data": WalletBalance }.

FieldTypeConstraints
wallet_idstring (uuid)required
balanceMoneyFormattedrequired
POST /v3/pass/me/band/lost #
bearer pass_fan credential

the fan freezes their own wristband.

The most time-critical thing anybody does in this product: a lost band is somebody else holding your money, at a bar, now. Waiting in a queue at the Pass desk to have a staff member do this is the difference between losing nothing and losing the balance. BandService::transition busts the tap cache synchronously, so the next tap on that serial is refused rather than refused-eventually.

No serial in the body. The band is resolved from the principal, so a fan can freeze exactly one wristband — their own — and a mistyped or guessed serial cannot freeze a stranger's night out. Unfreezing is deliberately NOT here: lost -> active is a staff decision made by somebody looking at the person and the band together.

Response

Returns { "data": PassFanBand }.

FieldTypeConstraints
serial_numberstringrequired
statusBandStatusrequired
GET /v3/pass/me/payment-intent/{id} #
bearer pass_fan credential

Path parameters

NameTypeNotes
idstring

Response

Returns { "data": PaymentIntent }.

FieldTypeConstraints
payment_intent_idstring (uuid)required
statestringrequired
gatewaystringrequired
amountobjectrequired
amount.amount_minorintegerrequired
amount.currencystringrequiredmin length 3 · max length 3
instructionsstringoptional
GET /v3/pass/me/statement #
bearer pass_fan credential

where the money went.

The one screen a fan opens when they think they were overcharged, so it reads the LEDGER rather than any per-event summary: the entries are what a dispute is settled against, and a second aggregate that disagreed with them would be worse than none.

Response

Returns { "data": [WalletStatementRow] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
typestringrequired
statestringrequired
directionstring | nulloptional
amountMoneyFormattedrequired
balance_afterMoneyFormattedrequired
descriptionstring | nulloptional
studentanyoptional
created_atstring (date-time)required
GET /v3/pass/me/tickets #
bearer pass_fan credential

the rest of them, cursor-paged.

Response

Returns { "data": [PassFanTicket] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
ticket_refstringrequired
ticket_typestring | nullrequired
statusstringrequiredissued blocked refunded
tier_namestring | nullrequired
band_serialstring | nullrequired
band_statusstring | nullrequired
checked_inbooleanrequired
eventobjectrequired
event.idstring (uuid)required
event.namestringrequired
event.slugstringrequired
event.starts_atstring | nullrequired
event.timezonestringrequired
event.settingsobjectrequired
created_atstringrequired
POST /v3/pass/me/topup #
bearer pass_fan idempotency credential

the fan preloads their own wallet.

The SAME PaymentIntentService::collect the parent app tops up a student through: an STK push, idempotent on the intent id so a retry on a venue's wifi cannot fire a second one. Nothing about money is reimplemented here; what this method adds is that the wallet is resolved from the PRINCIPAL rather than named in the body.

That is why preloading needs the wallet to exist at ticket-import time (SpendWallets): this endpoint funds a wallet, it does not conjure one. It will open theirs on first use as a backstop, for the fan whose ticket landed before that change.

Request body

FieldTypeConstraints
amount_minorintegerrequiredmin 1 · max -9223372036854775808
gatewaystringrequired
msisdnstringrequiredmax length 20
event_idstring | nulloptional

Response

Returns { "data": PaymentIntent }.

FieldTypeConstraints
payment_intent_idstring (uuid)required
statestringrequired
gatewaystringrequired
amountobjectrequired
amount.amount_minorintegerrequired
amount.currencystringrequiredmin length 3 · max length 3
instructionsstringoptional
POST /v3/pass/me/withdraw #
bearer pass_fan idempotency credential

the fan takes their leftover balance back out (D6, 0138).

The other half of "does leftover money follow the fan": it follows them, and when they want it in cash it goes to their M-Pesa. Owner's decisions, 2026-08-09 — the destination is the number ON THE ACCOUNT and cannot be named by the caller, a withdrawal may be made at any time rather than only after doors close, and the B2C send cost comes off the fan's amount rather than the organiser's pocket.

Almost none of this is new machinery. PaymentIntentService::payout() already holds the money BEFORE the provider is asked to send it, already puts the intent under a durable settlement watcher, and already reverses the hold when a send fails or times out. What is new is a fan-shaped door onto it, with a fee.

Request body

FieldTypeConstraints
amount_minorintegerrequiredmin 1 · max 100000000

Response

Returns { "data": … }; this endpoint's payload is not yet pinned to a named shape.

GET /v3/pass/organizers #
bearer pass_app pass_web device credential

Response

Returns { "data": [PassOrganizer] } with meta.next_cursor for the next page.

FieldTypeConstraints
idstring (uuid)required
namestringrequired
codestring | nullrequired
productstringrequired
POST /v3/pass/shifts/{id}/close #
bearer pass_app device credential

Path parameters

NameTypeNotes
idstring

Request body

FieldTypeConstraints
takings_minorintegerrequiredmin 0
salesintegerrequiredmin 0
entries_grantedinteger | nulloptionalmin 0
entries_deniedinteger | nulloptionalmin 0
by_methodobject[]optional

Response

Returns { "data": PassShift }.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
zone_idstring | nullrequired
user_idstring (uuid)required
device_serialstring | nullrequired
statusstringrequired
opened_atstringrequired
closed_atstring | nullrequired
takings_minorintegerrequired
salesintegerrequired
ledgered_takings_minorintegerrequired
ledgered_salesintegerrequired
entries_grantedintegerrequired
entries_deniedintegerrequired
by_methodobject[]required
POST /v3/pass/shifts/{id}/declare #
bearer pass_app device credential

Path parameters

NameTypeNotes
idstring

Request body

FieldTypeConstraints
takings_minorintegerrequiredmin 0
salesintegerrequiredmin 0
entries_grantedinteger | nulloptionalmin 0
entries_deniedinteger | nulloptionalmin 0
by_methodobject[]optional

Response

Returns { "data": PassShift }.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
zone_idstring | nullrequired
user_idstring (uuid)required
device_serialstring | nullrequired
statusstringrequired
opened_atstringrequired
closed_atstring | nullrequired
takings_minorintegerrequired
salesintegerrequired
ledgered_takings_minorintegerrequired
ledgered_salesintegerrequired
entries_grantedintegerrequired
entries_deniedintegerrequired
by_methodobject[]required
DELETE /v3/pass/ticketing #
bearer pass_web device credential

DELETE /v3/pass/ticketing

Response

Returns { "data": Ack }.

FieldTypeConstraints
okbooleanrequired
GET /v3/pass/ticketing #
bearer pass_web device credential

the card: status, counts, recent activity.

Response

Returns { "data": TicketingStatus }.

FieldTypeConstraints
connectedbooleanrequired
providerstringrequired
idstring | nullrequired
display_namestring | nullrequired
statusstring | nullrequired
enabledbooleanrequired
environmentstring | nullrequired
key_typestring | nullrequired
webhook_urlstring | nullrequired
last_synced_atstring | nullrequired
countsobject | nullrequired
counts.eventsintegerrequired
counts.ticketsintegerrequired
counts.webhook_events_24hintegerrequired
last_syncobject | nullrequired
last_sync.statusstringrequired
last_sync.started_atstring | nullrequired
last_sync.finished_atstring | nullrequired
last_sync.events_syncedintegerrequired
last_sync.tickets_syncedintegerrequired
last_sync.errorstring | nullrequired
GET /v3/pass/ticketing/activity #
bearer pass_web device credential

the card's expander: recent sync runs and webhook deliveries, newest first. Read-only projections of 0125's append-only tables; `credentials` is not among the columns, as ever.

Response

Returns { "data": TicketingActivity }.

FieldTypeConstraints
sync_logsTicketingSyncLog[]required
webhook_eventsTicketingWebhookDelivery[]required
POST /v3/pass/ticketing/connect #
bearer pass_web device credential

POST /v3/pass/ticketing/connect

Request body

FieldTypeConstraints
api_keystringrequiredmin length 10 · max length 512
environmentstringoptionalprod dev
seller_idstring | nulloptionalmax length 160
display_namestring | nulloptionalmax length 120

Response

Returns { "data": TicketingConnected }.

FieldTypeConstraints
integration_idstring (uuid)required
display_namestringrequired
key_typestringrequired
events_syncedintegerrequired
tickets_syncedintegerrequired
POST /v3/pass/ticketing/pause #
bearer pass_web device credential

POST /v3/pass/ticketing/pause {paused}

Request body

FieldTypeConstraints
pausedbooleanrequired

Response

Returns { "data": Ack }.

FieldTypeConstraints
okbooleanrequired
POST /v3/pass/ticketing/sync #
bearer pass_web device credential

the card's "Sync now".

Response

Returns { "data": TicketingSyncReport }.

FieldTypeConstraints
events_syncedintegerrequired
tickets_syncedintegerrequired
POST /v3/pass/ticketing/test #
bearer pass_web device credential

validate a key without storing it.

Request body

FieldTypeConstraints
api_keystringrequiredmin length 10 · max length 512
environmentstringoptionalprod dev
seller_idstring | nulloptionalmax length 160

Response

Returns { "data": TicketingProbe }.

FieldTypeConstraints
okbooleanrequired
reasonstring | nullrequired
environmentstring | nullrequired
key_typestring | nullrequired
display_namestring | nullrequired
event_countinteger | nullrequired
detected_environmentstring | nullrequired
errorstring | nullrequired
DELETE /v3/pass/tiers/{id} #
bearer pass_web settings.manage device credential

Path parameters

NameTypeNotes
idstring

Response

Returns { "data": … }; this endpoint's payload is not yet pinned to a named shape.

PATCH /v3/pass/tiers/{id} #
bearer pass_web settings.manage device credential

Path parameters

NameTypeNotes
idstring

Request body

FieldTypeConstraints
namestringrequiredmax length 120
slugstring | nulloptionalmax length 120
zone_idsstring (uuid)[]requiredmin items 1
positioninteger | nulloptionalmin 0 · max 999
colourstring | nulloptionalmax length 32
update_maskstring[]requiredmin items 1
expected_row_versionintegerrequiredmin 1

Response

Returns { "data": PassTier }.

FieldTypeConstraints
idstring (uuid)required
event_idstring (uuid)required
namestringrequired
slugstringrequired
positionintegerrequired
colourstring | nullrequired
zone_idsstring (uuid)[]required
created_atstringrequired
updated_atstringrequired
row_versionintegerrequired
GET /v3/pass/vendors #
bearer pass_web device credential

Query parameters

NameTypeConstraints
searchstring | nulloptionalmax length 120

Response

Returns { "data": [PassVendor] } with meta.next_cursor for the next page.

FieldTypeConstraints
wallet_idstring (uuid)required
namestringrequired
numberstring | nullrequired
phonestring | nullrequired
emailstring | nullrequired
notestring | nullrequired
taken_minorintegerrequired
currency_codestringrequired
statusWalletStatusrequired
created_atstring (date-time)required
POST /v3/pass/vendors #
bearer pass_web device credential

a trader starts trading.

The console has offered this since #287 and terra answered 405: the button called a route that was never shipped, so an organiser setting up a stall an hour before doors was sent to another product's admin to mint the wallet.

A vendor IS a merchant wallet with a nameplate, so this creates both — there is no vendor row to create first and fund later, which is the same reason the list reports taken_minor as the wallet's balance rather than a figure kept beside it.

Request body

FieldTypeConstraints
namestringrequiredmax length 160
phonestring | nulloptionalmax length 32
emailstring | nulloptionalmax length 160
notestring | nulloptionalmax length 500
zone_idstring | nulloptional

Response

Returns { "data": PassVendor }.

FieldTypeConstraints
wallet_idstring (uuid)required
namestringrequired
numberstring | nullrequired
phonestring | nullrequired
emailstring | nullrequired
notestring | nullrequired
taken_minorintegerrequired
currency_codestringrequired
statusWalletStatusrequired
created_atstring (date-time)required
GET /v3/webhook/ticketing/{integration} #
public 120,1

Providers ping with GET to prove the URL is alive before saving it.

Path parameters

NameTypeNotes
integrationstring

Response

Returns { "data": … }; this endpoint's payload is not yet pinned to a named shape.

POST /v3/webhook/ticketing/{integration} #
public 600,1

Path parameters

NameTypeNotes
integrationstring

Response

Returns { "data": … }; this endpoint's payload is not yet pinned to a named shape.