Host console — see who bought tickets & who joined
How a host moves from their list of experiences → one event's dashboard → the buyers list → the door (show the QR / scan a guest) → the live checked-in roster. Every step is tied to a real endpoint (built) or a planned one (to add), and to the matching mockup in design/index.html.
design/index.html) — this flow lives on
55 Host home58 Host scanner · door56 Broadcast59 Venue offer builder, with edges on 50 Dispute a check-in39 Sold out · waitlist.01 · The host lifecycle
Ticketing & check-in only matter after an experience is created and published. This page picks up at "it's live" and follows the host through the two questions they actually ask on event day: who bought? and who's here?
qrCodeToken (the check-in code) and accumulates ExperienceTicket rows (buyers) and ExperienceAttendance rows (joined). "Who bought" reads tickets; "who joined" reads attendance; the door either shows that token (built) or scans the guest's ticket token (to add).02 · Step 1 — My experiences
Every experience the host owns — with its check-in code
The host lands on their own list. Each card is compact (listing fields only) and — because this list is host-only — also carries qrToken, the check-in code. That's why there's no separate "get QR" endpoint: the code rides along here.
data[] = { id, title, coverImage, startTime, status, locationName, currentGuests, capacity, qrToken } · meta { page, limit, total, totalPages }
NoteqrToken is null until the experience is published (that's when it's minted).
curl "$BASE/experiences/mine" -H "Authorization: Bearer $HOST_TOKEN" \
| jq '.data[] | {id, title, status, currentGuests, capacity, qrToken}'
/mine only returns currentGuests (checked-in), not sold. Add a soldCount to each card, or fetch it from the per-event dashboard (next step).03 · Step 2 — One event's dashboard
Sold, revenue, checked-in — at a glance
Opening one experience shows its numbers. Today the host would stitch this from three calls (ticket-holders meta for sold+revenue, attendees meta for checked-in, GET /:id for tiers). One dashboard endpoint collapses that into a single read.
ticket-holders meta{soldTotal,revenueCents} + attendees meta{currentGuests,capacity} + GET /:id tiers built
Returns{ sold, capacity, checkedIn, noShows, revenueCents, pctSold, byTier:[{tierId,name,sold,remaining,revenueCents}], recentCheckIns:[…] }
/dashboard exists, the client composes the header from the two list endpoints' meta — nothing blocks the screen; the new endpoint is a convenience + one round-trip.04 · Step 3 — Who bought tickets
The buyers list — each with a "checked in?" flag
Every ticket purchased, newest first, with the buyer, tier, price, status, and whether they've already walked in. Paginated and filterable by ticket status; the meta carries the totals the dashboard shows.
?page=1&limit=20&status=VALID|USED
Row{ ticketId, tierId, tierName, price, currency, status, checkedIn, purchasedAt, usedAt, user:{id,name,avatarUrl} }
Meta{ page, limit, total, totalPages, soldTotal, revenueCents }
curl "$BASE/experiences/1/ticket-holders?status=VALID&page=1&limit=20" \
-H "Authorization: Bearer $HOST_TOKEN" | jq '{meta, rows: .data | length}'
loadOwned() 404s if the caller isn't the experience's host, so one host can never read another's buyer list.05 · Step 4 — The door: show the QR / scan a guest
Two check-in models — pick one (or support both)
The design's screen 58 is a door scanner: the host points the phone at the guest's ticket QR. The built flow is the mirror image: the host displays the venue's qrToken and the guest scans it. Both end at the same write (attendance + ticket USED); they differ only in who scans whom.
POST /experiences/check-in with qrToken). Design screen 58 = host scans guest. To match the mockup, add a host-initiated scan; keep the built one for signage/self-serve.data[].qrToken, rendered as a QR at the door
RotatePOST /api/experiences/:id/qr/rotate built — invalidates old screenshots
Host scanPOST /api/experiences/:id/scan to add — body { ticketToken } (the guest's), checks them in host-side
// POST /api/experiences/:id/scan (host-initiated, matches screen 58)
// body: { "ticketToken": "<from the guest's ticket QR>" }
// 200 → {
// guest: { id, name, avatarUrl },
// ticket: { id, tierName, status: "USED" },
// offer: { label: "2-for-1", redeemed: true } | null,
// currentGuests: 7
// }
// guards: ticket belongs to THIS experience · status VALID · not already in
ticketToken (resolve → must belong to :id, be VALID, owner not already checked in) instead of the attendee validating the venue token. One write path (ExperienceAttendance + ticket → USED + currentGuests += 1) serves both.06 · Step 5 — Who joined (checked-in roster)
The live roster — who's actually in the room
The attendance list, newest check-in first, with each guest and the time they walked in. The meta carries the live headcount and capacity — that's the "6 / 10" in the header. Cross-referencing with ticket-holders gives the "bought but not yet in" set.
?page=1&limit=20
Row{ userId, checkedInAt, ticketId, user:{id,name,avatarUrl} }
Meta{ page, limit, total, totalPages, currentGuests, capacity }
curl "$BASE/experiences/1/attendees?page=1&limit=20" \
-H "Authorization: Bearer $HOST_TOKEN" | jq '{in: .meta.currentGuests, cap: .meta.capacity}'
experience:checkin socket event so "6/10" ticks without refresh) · ② undo a mis-scan (screen 50 Dispute) · ③ a "not yet in" view (a diff of tickets − attendance).07 · Navigation map & event-day sequence
Every arrow is a real transition from the prototype's link map in design/index.html — the label is the button/tap that fires it. The venue console's bottom tab bar wires 60 Dashboard ⇄ 59 Offers ⇄ 58 Check-ins (Payouts tab is present but unlinked).
Event-day sequence
Both check-in models on one timeline. Green = built, gold = to add.
08 · API plan — the full host-side build list
Everything the host side needs, split by status. Group A already ships the "see tickets & joined users" story end-to-end; Group B rounds out this flow to match the mockups; Group C is the wider host surface.
A · Built today live
| Method · path | Does | Screen |
|---|---|---|
GET /experiences/mine | Host's own experiences (cards + qrToken), paginated | 55 |
GET /experiences/:id | Detail; owner can view own drafts; carries tiers + host rating | 38 · 55 |
GET /experiences/:id/ticket-holders | Who bought — buyers + tier + checkedIn flag; meta.soldTotal, meta.revenueCents | 55 |
GET /experiences/:id/attendees | Who joined — checked-in roster; meta.currentGuests, meta.capacity | 58 |
POST /experiences/:id/qr/rotate | Mint a fresh check-in code (invalidate old QR screenshots) | 58 |
POST /experiences/:id/publish | Go live (DRAFT→PUBLISHED) — mints the qrCodeToken | 54 |
B · To add — core to this flow next
| Method · path | Why | Shape | Screen |
|---|---|---|---|
GET /experiences/:id/dashboard | One read for the event header instead of stitching 3 calls | { sold, capacity, checkedIn, noShows, revenueCents, pctSold, byTier[] } | 55·60 |
POST /experiences/:id/scan | Host-initiated door check-in — matches screen 58 (host scans guest) | body { ticketToken } → { guest, ticket, offer, currentGuests } | 58 |
POST /experiences/:id/attendees/:userId/undo | Reverse a mis-scan / mark no-show | attendance removed, ticket→VALID, currentGuests−1 | 50 |
socket experience:checkin | Push live headcount to the host room (no polling) | { experienceId, currentGuests, guest } | 58 |
GET /experiences/:id/not-checked-in | Bought-but-not-yet-in (tickets − attendance) for door reconciliation | holder rows where checkedIn=false | 58 |
C · Later — broader host surface backlog
| Method · path | Why | Screen |
|---|---|---|
POST /experiences/:id/broadcast | Host→attendee push, audience-scoped (ATTENDEES/FOLLOWERS/BOTH), rate-limited | 56·57 |
GET /experiences/:id/ticket-holders/export | CSV guest list for the door / records | 55 |
GET /experiences/:id/offer/redemptions | Track ExperienceHostOffer perk redemptions at scan | 58·59 |
POST /experiences/:id/waitlist · GET …/waitlist | Join / list the waitlist when every tier is sold out | 39 |
GET /experiences/:id/payouts | Revenue reconciliation — belongs to the money module, referenced here | 58 (Payouts) |
/mine → /ticket-holders → /attendees). Group B is what turns those reads into the polished screen-58 door experience (host-scan, live count, undo, one-call dashboard).09 · Data model touchpoints
Nothing new is needed for Group A; Group B reuses the same tables. The read/write map:
| Model | Read by | Written by |
|---|---|---|
Experience (qrCodeToken, currentGuests, capacity, status) | /mine, /:id, /dashboard | publish (mint token), qr/rotate, check-in (currentGuests) |
ExperienceTicket (buyer, tier, status, ticketToken) | /ticket-holders | purchase (VALID), check-in / scan (USED), undo (back to VALID) |
ExperienceAttendance (@@unique(experienceId,userId)) | /attendees, /dashboard | check-in / scan (create), undo (delete) |
ExperienceTicketTier | /:id, /dashboard (byTier) | tickets create, purchase (quantitySold++) |
ExperienceHostOffer | /:id, redemptions (later) | scan (redeem perk — later) |
loadOwned(hostId, id), which 404s a non-owner. The buyer/attendee lists expose only { id, name, avatarUrl } per user — never contact details.