Pass
TerraPass — events, doorlists, tiers, shifts, the till and the fan surface.
56 endpoints
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
| Field | Type | Constraints | |
|---|---|---|---|
phone | string | null | optional | — |
email | string | null | optional | max length 160 |
Response
Returns { "data": PassFanClaim }.
| Field | Type | Constraints | |
|---|---|---|---|
sent | boolean | required | — |
challenge_id | string | null | required | — |
expires_in | integer | null | required | — |
code → a `pass_fan` session.
Request body
| Field | Type | Constraints | |
|---|---|---|---|
challenge_id | string (uuid) | required | — |
otp | string | required | min length 6 · max length 6 |
Response
Returns { "data": AuthTokens }.
| Field | Type | Constraints | |
|---|---|---|---|
access_token | string | required | — |
token_type | string | required | — |
expires_in | integer | required | — |
refresh_token | string | required | — |
`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
| Name | Type | Notes |
|---|---|---|
serial | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
organizer_id | string (uuid) | required | — |
Response
Returns { "data": BandMovedToOrganizer }.
| Field | Type | Constraints | |
|---|---|---|---|
serial_number | string | required | — |
status | string | required | — |
organizer_id | string (uuid) | required | — |
`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
| Name | Type | Constraints | |
|---|---|---|---|
zone | string | null | optional | — |
Response
Returns { "data": PassCatalogue }.
| Field | Type | Constraints | |
|---|---|---|---|
categories | object[] | required | — |
items | object[] | required | — |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
text | string | required | min length 3 · max length 12000 |
Response
Returns { "data": VendorListProposal }.
| Field | Type | Constraints | |
|---|---|---|---|
items | object[] | required | — |
skipped | integer | required | — |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
serial_number | string | required | max length 64 |
name | string | null | optional | max length 120 |
zone_id | string | null | optional | — |
model | string | null | optional | max length 120 |
firmware | string | null | optional | max length 64 |
Response
Returns { "data": PassDevice }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
serial_number | string | required | — |
name | string | required | — |
zone_id | string | null | required | — |
status | string | required | — |
registered | boolean | required | — |
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
| Name | Type | Constraints | |
|---|---|---|---|
status | string | null | optional | max length 40 |
kind | string | null | optional | max length 40 |
zone_id | string | null | optional | — |
q | string | null | optional | max length 120 |
Response
Returns { "data": [PassDeviceRow] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
name | string | required | — |
serial_number | string | required | — |
kind | DeviceKind | required | — |
status | DeviceStatus | required | — |
zone | object | null | required | — |
zone.id | string (uuid) | required | — |
zone.name | string | required | — |
battery_pct | integer | null | required | — |
signal_strength | integer | null | required | — |
sim_present | boolean | null | required | — |
firmware | string | null | required | — |
presence | string | required | — |
last_seen_at | string | null | required | — |
row_version | integer | required | — |
keyset page, newest first.
Response
Returns { "data": [PassEvent] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
organizer_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
zone_id | string | null | required | — |
status | string | required | — |
starts_at | string | null | required | — |
ends_at | string | null | required | — |
timezone | string | required | — |
currency_code | string | required | — |
balance_carries | boolean | required | — |
credential_grace_minutes | integer | required | — |
settings | object | required | — |
created_at | string | required | — |
row_version | integer | required | — |
mints the event AND its root zone together.
Request body
| Field | Type | Constraints | |
|---|---|---|---|
name | string | required | max length 160 |
slug | string | null | optional | max length 160 |
starts_at | string | null | optional | — |
ends_at | string | null | optional | — |
timezone | string | null | optional | — |
status | string | null | optional | draft published live settling closed cancelled |
currency_code | string | null | optional | min length 3 · max length 3 |
balance_carries | boolean | null | optional | — |
credential_grace_minutes | integer | null | optional | min 0 · max 10080 |
settings | object | null | optional | — |
Response
Returns { "data": PassEvent }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
organizer_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
zone_id | string | null | required | — |
status | string | required | — |
starts_at | string | null | required | — |
ends_at | string | null | required | — |
timezone | string | required | — |
currency_code | string | required | — |
balance_carries | boolean | required | — |
credential_grace_minutes | integer | required | — |
settings | object | required | — |
created_at | string | required | — |
row_version | integer | required | — |
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
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": PassCashDesk }.
| Field | Type | Constraints | |
|---|---|---|---|
taken_minor | integer | required | — |
topups | integer | required | — |
refunds_minor | integer | required | — |
currency | string | required | — |
keyset page, newest first.
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Query parameters
| Name | Type | Constraints | |
|---|---|---|---|
search | string | null | optional | max length 120 |
Response
Returns { "data": [PassGuest] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
account_id | string (uuid) | required | — |
name | string | required | — |
phone | string | null | required | — |
email | string | null | required | — |
ticket_ref | string | required | — |
ticket_type | string | null | required | — |
tier_id | string | null | required | — |
tier_name | string | null | required | — |
status | string | required | issued blocked refunded |
source | string | required | — |
holder_confirmed | boolean | required | — |
holder_confirmed_at | string | null | required | — |
band_serial | string | null | required | — |
band_status | string | null | required | — |
checked_in | boolean | required | — |
wallet | any | required | — |
notes | string | null | required | — |
created_at | string | required | — |
row_version | integer | required | — |
GET /v3/pass/events/{eventId}/guests/{id}
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string | |
id | string |
Response
Returns { "data": PassGuest }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
account_id | string (uuid) | required | — |
name | string | required | — |
phone | string | null | required | — |
email | string | null | required | — |
ticket_ref | string | required | — |
ticket_type | string | null | required | — |
tier_id | string | null | required | — |
tier_name | string | null | required | — |
status | string | required | issued blocked refunded |
source | string | required | — |
holder_confirmed | boolean | required | — |
holder_confirmed_at | string | null | required | — |
band_serial | string | null | required | — |
band_status | string | null | required | — |
checked_in | boolean | required | — |
wallet | any | required | — |
notes | string | null | required | — |
created_at | string | required | — |
row_version | integer | required | — |
GET /v3/pass/events/{eventId}/history?days=1&device=TG-0042
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Query parameters
| Name | Type | Constraints | |
|---|---|---|---|
days | integer | null | optional | min 0 · max 365 |
device | string | null | optional | max length 64 |
zone | string | null | optional | — |
Response
Returns { "data": PassHistory }.
| Field | Type | Constraints | |
|---|---|---|---|
sales | object[] | required | — |
scans | object[] | required | — |
the console's headline tiles.
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": PassEventOverview }.
| Field | Type | Constraints | |
|---|---|---|---|
tickets_total | integer | required | — |
tickets_banded | integer | required | — |
scans_allowed | integer | required | — |
scans_denied | integer | required | — |
sales_count | integer | required | — |
sales_minor | integer | required | — |
currency | string | required | — |
GET /v3/pass/events/{eventId}/report?days=1&device=TG-0042
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Query parameters
| Name | Type | Constraints | |
|---|---|---|---|
days | integer | null | optional | min 0 · max 365 |
device | string | null | optional | max length 64 |
zone | string | null | optional | — |
Response
Returns { "data": PassReport }.
| Field | Type | Constraints | |
|---|---|---|---|
currency | string | required | — |
net_minor | integer | required | — |
sales_count | integer | required | — |
items_sold | integer | required | — |
buckets | object[] | required | — |
top_sellers | object[] | required | — |
what each outlet is owed (T-7).
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": PassSettlements }.
| Field | Type | Constraints | |
|---|---|---|---|
currency | string | required | — |
total_minor | integer | required | — |
outlets | object[] | required | — |
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
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": PassShift }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
zone_id | string | null | required | — |
user_id | string (uuid) | required | — |
device_serial | string | null | required | — |
status | string | required | — |
opened_at | string | required | — |
closed_at | string | null | required | — |
takings_minor | integer | required | — |
sales | integer | required | — |
ledgered_takings_minor | integer | required | — |
ledgered_sales | integer | required | — |
entries_granted | integer | required | — |
entries_denied | integer | required | — |
by_method | object[] | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
zone_id | string | null | optional | — |
device_serial | string | null | optional | max length 120 |
Response
Returns { "data": PassShift }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
zone_id | string | null | required | — |
user_id | string (uuid) | required | — |
device_serial | string | null | required | — |
status | string | required | — |
opened_at | string | required | — |
closed_at | string | null | required | — |
takings_minor | integer | required | — |
sales | integer | required | — |
ledgered_takings_minor | integer | required | — |
ledgered_sales | integer | required | — |
entries_granted | integer | required | — |
entries_denied | integer | required | — |
by_method | object[] | required | — |
Every shift on an event — the supervisor's cash-up view.
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": [PassShift] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
zone_id | string | null | required | — |
user_id | string (uuid) | required | — |
device_serial | string | null | required | — |
status | string | required | — |
opened_at | string | required | — |
closed_at | string | null | required | — |
takings_minor | integer | required | — |
sales | integer | required | — |
ledgered_takings_minor | integer | required | — |
ledgered_sales | integer | required | — |
entries_granted | integer | required | — |
entries_denied | integer | required | — |
by_method | object[] | required | — |
POST /v3/pass/events/{eventId}/ticket/scan
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
payload | string | required | min length 8 · max length 4096 |
Response
Returns { "data": PassTicketScan }.
| Field | Type | Constraints | |
|---|---|---|---|
verdict | string | required | ok ok_unverified already_used invalid unknown not_configured |
ticket | any | required | — |
redeemed_at | string | null | required | — |
a comp, crew, or a door sale.
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
first_name | string | required | max length 80 |
last_name | string | null | optional | max length 80 |
phone | string | null | optional | — |
email | string | null | optional | max length 160 |
ticket_ref | string | null | optional | max length 80 |
external_ref | string | null | optional | max length 120 |
ticket_type | string | null | optional | max length 60 |
tier_id | string | null | optional | — |
notes | string | null | optional | max length 500 |
Response
Returns { "data": PassGuest }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
account_id | string (uuid) | required | — |
name | string | required | — |
phone | string | null | required | — |
email | string | null | required | — |
ticket_ref | string | required | — |
ticket_type | string | null | required | — |
tier_id | string | null | required | — |
tier_name | string | null | required | — |
status | string | required | issued blocked refunded |
source | string | required | — |
holder_confirmed | boolean | required | — |
holder_confirmed_at | string | null | required | — |
band_serial | string | null | required | — |
band_status | string | null | required | — |
checked_in | boolean | required | — |
wallet | any | required | — |
notes | string | null | required | — |
created_at | string | required | — |
row_version | integer | required | — |
off the doorlist, kept for the record.
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string | |
id | string |
Response
Returns { "data": Deleted }.
| Field | Type | Constraints | |
|---|---|---|---|
deleted | boolean | required | — |
type, tier, status, notes.
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string | |
id | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
ticket_type | string | null | optional | max length 60 |
tier_id | string | null | optional | — |
status | string | optional | issued blocked refunded |
notes | string | null | optional | max length 500 |
update_mask | string[] | required | min items 1 |
expected_row_version | integer | required | min 1 |
Response
Returns { "data": PassGuest }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
account_id | string (uuid) | required | — |
name | string | required | — |
phone | string | null | required | — |
email | string | null | required | — |
ticket_ref | string | required | — |
ticket_type | string | null | required | — |
tier_id | string | null | required | — |
tier_name | string | null | required | — |
status | string | required | issued blocked refunded |
source | string | required | — |
holder_confirmed | boolean | required | — |
holder_confirmed_at | string | null | required | — |
band_serial | string | null | required | — |
band_status | string | null | required | — |
checked_in | boolean | required | — |
wallet | any | required | — |
notes | string | null | required | — |
created_at | string | required | — |
row_version | integer | required | — |
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
| Name | Type | Notes |
|---|---|---|
eventId | string | |
id | string |
Response
Returns { "data": [PassTicketHolderMove] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
kind | string | required | claim transfer correction |
channel | string | null | required | — |
note | string | null | required | — |
from | any | required | — |
to | PassTicketHolderParty | required | — |
at | string | required | — |
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
| Name | Type | Notes |
|---|---|---|
eventId | string | |
id | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
kind | string | required | claim transfer correction |
first_name | string | required | max length 80 |
last_name | string | null | optional | max length 80 |
phone | string | null | optional | — |
email | string | null | optional | max length 160 |
note | string | null | optional | max length 500 |
Response
Returns { "data": PassGuest }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
account_id | string (uuid) | required | — |
name | string | required | — |
phone | string | null | required | — |
email | string | null | required | — |
ticket_ref | string | required | — |
ticket_type | string | null | required | — |
tier_id | string | null | required | — |
tier_name | string | null | required | — |
status | string | required | issued blocked refunded |
source | string | required | — |
holder_confirmed | boolean | required | — |
holder_confirmed_at | string | null | required | — |
band_serial | string | null | required | — |
band_status | string | null | required | — |
checked_in | boolean | required | — |
wallet | any | required | — |
notes | string | null | required | — |
created_at | string | required | — |
row_version | integer | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": [PassTier] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
position | integer | required | — |
colour | string | null | required | — |
zone_ids | string (uuid)[] | required | — |
created_at | string | required | — |
updated_at | string | required | — |
row_version | integer | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
name | string | required | max length 120 |
slug | string | null | optional | max length 120 |
zone_ids | string (uuid)[] | required | min items 1 |
position | integer | null | optional | min 0 · max 999 |
colour | string | null | optional | max length 32 |
Response
Returns { "data": PassTier }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
position | integer | required | — |
colour | string | null | required | — |
zone_ids | string (uuid)[] | required | — |
created_at | string | required | — |
updated_at | string | required | — |
row_version | integer | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
eventId | string |
Response
Returns { "data": [PassZone] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
name | string | required | — |
kind | ZoneKind | required | — |
parent_zone_id | string | null | required | — |
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
| Name | Type | Notes |
|---|---|---|
eventId | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
name | string | required | max length 120 |
kind | string | required | gate outlet office other |
parent_zone_id | string | null | optional | — |
Response
Returns { "data": PassZone }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
name | string | required | — |
kind | ZoneKind | required | — |
parent_zone_id | string | null | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Response
Returns { "data": … }; this endpoint's payload is not yet
pinned to a named shape.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Response
Returns { "data": PassEvent }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
organizer_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
zone_id | string | null | required | — |
status | string | required | — |
starts_at | string | null | required | — |
ends_at | string | null | required | — |
timezone | string | required | — |
currency_code | string | required | — |
balance_carries | boolean | required | — |
credential_grace_minutes | integer | required | — |
settings | object | required | — |
created_at | string | required | — |
row_version | integer | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
name | string | optional | max length 160 |
slug | string | null | optional | max length 160 |
starts_at | string | null | optional | — |
ends_at | string | null | optional | — |
timezone | string | null | optional | — |
status | string | null | optional | draft published live settling closed cancelled |
currency_code | string | null | optional | min length 3 · max length 3 |
balance_carries | boolean | null | optional | — |
credential_grace_minutes | integer | null | optional | min 0 · max 10080 |
settings | object | null | optional | — |
update_mask | string[] | required | min items 1 |
expected_row_version | integer | required | min 1 |
Response
Returns { "data": PassEvent }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
organizer_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
zone_id | string | null | required | — |
status | string | required | — |
starts_at | string | null | required | — |
ends_at | string | null | required | — |
timezone | string | required | — |
currency_code | string | required | — |
balance_carries | boolean | required | — |
credential_grace_minutes | integer | required | — |
settings | object | required | — |
created_at | string | required | — |
row_version | integer | required | — |
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 }.
| Field | Type | Constraints | |
|---|---|---|---|
account | object | required | — |
account.id | string (uuid) | required | — |
account.name | string | required | — |
account.phone | string | null | required | — |
account.email | string | null | required | — |
wallet | any | required | — |
tickets | PassFanTicket[] | required | — |
what they can spend at the bar right now.
Response
Returns { "data": WalletBalance }.
| Field | Type | Constraints | |
|---|---|---|---|
wallet_id | string (uuid) | required | — |
balance | MoneyFormatted | required | — |
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 }.
| Field | Type | Constraints | |
|---|---|---|---|
serial_number | string | required | — |
status | BandStatus | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Response
Returns { "data": PaymentIntent }.
| Field | Type | Constraints | |
|---|---|---|---|
payment_intent_id | string (uuid) | required | — |
state | string | required | — |
gateway | string | required | — |
amount | object | required | — |
amount.amount_minor | integer | required | — |
amount.currency | string | required | min length 3 · max length 3 |
instructions | string | optional | — |
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.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
type | string | required | — |
state | string | required | — |
direction | string | null | optional | — |
amount | MoneyFormatted | required | — |
balance_after | MoneyFormatted | required | — |
description | string | null | optional | — |
student | any | optional | — |
created_at | string (date-time) | required | — |
the rest of them, cursor-paged.
Response
Returns { "data": [PassFanTicket] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
ticket_ref | string | required | — |
ticket_type | string | null | required | — |
status | string | required | issued blocked refunded |
tier_name | string | null | required | — |
band_serial | string | null | required | — |
band_status | string | null | required | — |
checked_in | boolean | required | — |
event | object | required | — |
event.id | string (uuid) | required | — |
event.name | string | required | — |
event.slug | string | required | — |
event.starts_at | string | null | required | — |
event.timezone | string | required | — |
event.settings | object | required | — |
created_at | string | required | — |
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
| Field | Type | Constraints | |
|---|---|---|---|
amount_minor | integer | required | min 1 · max -9223372036854775808 |
gateway | string | required | — |
msisdn | string | required | max length 20 |
event_id | string | null | optional | — |
Response
Returns { "data": PaymentIntent }.
| Field | Type | Constraints | |
|---|---|---|---|
payment_intent_id | string (uuid) | required | — |
state | string | required | — |
gateway | string | required | — |
amount | object | required | — |
amount.amount_minor | integer | required | — |
amount.currency | string | required | min length 3 · max length 3 |
instructions | string | optional | — |
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
| Field | Type | Constraints | |
|---|---|---|---|
amount_minor | integer | required | min 1 · max 100000000 |
Response
Returns { "data": … }; this endpoint's payload is not yet
pinned to a named shape.
Response
Returns { "data": [PassOrganizer] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
name | string | required | — |
code | string | null | required | — |
product | string | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
takings_minor | integer | required | min 0 |
sales | integer | required | min 0 |
entries_granted | integer | null | optional | min 0 |
entries_denied | integer | null | optional | min 0 |
by_method | object[] | optional | — |
Response
Returns { "data": PassShift }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
zone_id | string | null | required | — |
user_id | string (uuid) | required | — |
device_serial | string | null | required | — |
status | string | required | — |
opened_at | string | required | — |
closed_at | string | null | required | — |
takings_minor | integer | required | — |
sales | integer | required | — |
ledgered_takings_minor | integer | required | — |
ledgered_sales | integer | required | — |
entries_granted | integer | required | — |
entries_denied | integer | required | — |
by_method | object[] | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
takings_minor | integer | required | min 0 |
sales | integer | required | min 0 |
entries_granted | integer | null | optional | min 0 |
entries_denied | integer | null | optional | min 0 |
by_method | object[] | optional | — |
Response
Returns { "data": PassShift }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
zone_id | string | null | required | — |
user_id | string (uuid) | required | — |
device_serial | string | null | required | — |
status | string | required | — |
opened_at | string | required | — |
closed_at | string | null | required | — |
takings_minor | integer | required | — |
sales | integer | required | — |
ledgered_takings_minor | integer | required | — |
ledgered_sales | integer | required | — |
entries_granted | integer | required | — |
entries_denied | integer | required | — |
by_method | object[] | required | — |
DELETE /v3/pass/ticketing
Response
Returns { "data": Ack }.
| Field | Type | Constraints | |
|---|---|---|---|
ok | boolean | required | — |
the card: status, counts, recent activity.
Response
Returns { "data": TicketingStatus }.
| Field | Type | Constraints | |
|---|---|---|---|
connected | boolean | required | — |
provider | string | required | — |
id | string | null | required | — |
display_name | string | null | required | — |
status | string | null | required | — |
enabled | boolean | required | — |
environment | string | null | required | — |
key_type | string | null | required | — |
webhook_url | string | null | required | — |
last_synced_at | string | null | required | — |
counts | object | null | required | — |
counts.events | integer | required | — |
counts.tickets | integer | required | — |
counts.webhook_events_24h | integer | required | — |
last_sync | object | null | required | — |
last_sync.status | string | required | — |
last_sync.started_at | string | null | required | — |
last_sync.finished_at | string | null | required | — |
last_sync.events_synced | integer | required | — |
last_sync.tickets_synced | integer | required | — |
last_sync.error | string | null | required | — |
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 }.
| Field | Type | Constraints | |
|---|---|---|---|
sync_logs | TicketingSyncLog[] | required | — |
webhook_events | TicketingWebhookDelivery[] | required | — |
POST /v3/pass/ticketing/connect
Request body
| Field | Type | Constraints | |
|---|---|---|---|
api_key | string | required | min length 10 · max length 512 |
environment | string | optional | prod dev |
seller_id | string | null | optional | max length 160 |
display_name | string | null | optional | max length 120 |
Response
Returns { "data": TicketingConnected }.
| Field | Type | Constraints | |
|---|---|---|---|
integration_id | string (uuid) | required | — |
display_name | string | required | — |
key_type | string | required | — |
events_synced | integer | required | — |
tickets_synced | integer | required | — |
POST /v3/pass/ticketing/pause {paused}
Request body
| Field | Type | Constraints | |
|---|---|---|---|
paused | boolean | required | — |
Response
Returns { "data": Ack }.
| Field | Type | Constraints | |
|---|---|---|---|
ok | boolean | required | — |
the card's "Sync now".
Response
Returns { "data": TicketingSyncReport }.
| Field | Type | Constraints | |
|---|---|---|---|
events_synced | integer | required | — |
tickets_synced | integer | required | — |
validate a key without storing it.
Request body
| Field | Type | Constraints | |
|---|---|---|---|
api_key | string | required | min length 10 · max length 512 |
environment | string | optional | prod dev |
seller_id | string | null | optional | max length 160 |
Response
Returns { "data": TicketingProbe }.
| Field | Type | Constraints | |
|---|---|---|---|
ok | boolean | required | — |
reason | string | null | required | — |
environment | string | null | required | — |
key_type | string | null | required | — |
display_name | string | null | required | — |
event_count | integer | null | required | — |
detected_environment | string | null | required | — |
error | string | null | required | — |
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Response
Returns { "data": … }; this endpoint's payload is not yet
pinned to a named shape.
Path parameters
| Name | Type | Notes |
|---|---|---|
id | string |
Request body
| Field | Type | Constraints | |
|---|---|---|---|
name | string | required | max length 120 |
slug | string | null | optional | max length 120 |
zone_ids | string (uuid)[] | required | min items 1 |
position | integer | null | optional | min 0 · max 999 |
colour | string | null | optional | max length 32 |
update_mask | string[] | required | min items 1 |
expected_row_version | integer | required | min 1 |
Response
Returns { "data": PassTier }.
| Field | Type | Constraints | |
|---|---|---|---|
id | string (uuid) | required | — |
event_id | string (uuid) | required | — |
name | string | required | — |
slug | string | required | — |
position | integer | required | — |
colour | string | null | required | — |
zone_ids | string (uuid)[] | required | — |
created_at | string | required | — |
updated_at | string | required | — |
row_version | integer | required | — |
Query parameters
| Name | Type | Constraints | |
|---|---|---|---|
search | string | null | optional | max length 120 |
Response
Returns { "data": [PassVendor] } with meta.next_cursor for the next page.
| Field | Type | Constraints | |
|---|---|---|---|
wallet_id | string (uuid) | required | — |
name | string | required | — |
number | string | null | required | — |
phone | string | null | required | — |
email | string | null | required | — |
note | string | null | required | — |
taken_minor | integer | required | — |
currency_code | string | required | — |
status | WalletStatus | required | — |
created_at | string (date-time) | required | — |
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
| Field | Type | Constraints | |
|---|---|---|---|
name | string | required | max length 160 |
phone | string | null | optional | max length 32 |
email | string | null | optional | max length 160 |
note | string | null | optional | max length 500 |
zone_id | string | null | optional | — |
Response
Returns { "data": PassVendor }.
| Field | Type | Constraints | |
|---|---|---|---|
wallet_id | string (uuid) | required | — |
name | string | required | — |
number | string | null | required | — |
phone | string | null | required | — |
email | string | null | required | — |
note | string | null | required | — |
taken_minor | integer | required | — |
currency_code | string | required | — |
status | WalletStatus | required | — |
created_at | string (date-time) | required | — |
Providers ping with GET to prove the URL is alive before saving it.
Path parameters
| Name | Type | Notes |
|---|---|---|
integration | string |
Response
Returns { "data": … }; this endpoint's payload is not yet
pinned to a named shape.
Path parameters
| Name | Type | Notes |
|---|---|---|
integration | string |
Response
Returns { "data": … }; this endpoint's payload is not yet
pinned to a named shape.