Sales and analytics
Every event has its own Sales page with all the numbers you need to understand what’s happening: how much you sold, where the buyers came from, which tier sells best, how many QRs already entered the venue.
How to get there
From three places in the dashboard:
- In Events (the list), every card has a View sales button.
- Inside an event editor, top-right next to the status badge.
- Direct URL:
/dashboard/events/<id>/sales.
Only the org owner has access. Anyone else gets 403.
Headline numbers
Four KPIs at the top:
| Card | What it means |
|---|---|
| Gross revenue | Sum of totalCents across all paid + fulfilled orders |
| Service fees collected | What you added on top as %. Lands in your Stripe too |
| Orders | How many paid orders (independent of ticket count) |
| Tickets sold | Sum of items.quantity — one buyer with 4 tickets counts as 4 here |
All amounts are formatted in the event currency and the active locale (es-MX, en-US, etc.).
QR status
A card shows the count by status:
- Issued — QR generated and delivered to the buyer, not yet validated.
- Validated — successfully scanned by an authorized device.
- Voided — manually invalidated (admin) or by anti-fraud.
- Refunded — order was reverted; the QR no longer enters.
During the event this tells you how many people entered (validated) vs how many are still pending (issued - validated).
Channel attribution (UTM)
Any link to your event can carry ?utm_source=<channel>&utm_medium=<medium>&utm_campaign=<campaign>. Boletra’s parser stores those parameters on the order, and the Sales page aggregates them in a table:
| Column | Value |
|---|---|
| Source | utm_source (instagram, x, mailchimp, …) |
| Medium | utm_medium (story, post, email, paid_ads, …) |
| Revenue | Sum of totalCents for that combination |
| Orders | How many orders that combination generated |
Without UTMs everything falls into direct. Some useful patterns:
?utm_source=instagram&utm_medium=story?utm_source=mailchimp&utm_medium=email&utm_campaign=presale?utm_source=ig&utm_medium=paid&utm_campaign=launch
Orders list
At the bottom there’s the paginated list of recent orders, sorted newest first. Per row you see:
- Buyer — name + email + status badge (pending / paid / fulfilled / refunded / failed).
- Tickets — breakdown
quantity × tier name. - Channel — UTM source + medium (or
—if direct). - Total — what the buyer paid (including service fee if any).
- Date — local timestamp.
Search
The input at the top filters by buyer email or name with a 300ms debounce (doesn’t hammer the API on every keystroke). Useful to find the order from someone reporting an issue.
Filters
The pills filter by status:
- All — default view.
- Paid — Stripe captured the payment but QRs aren’t issued yet (typically lasts milliseconds in prod).
- Fulfilled — paid + QRs issued + credits debited + email sent.
- Refunded — reverted via Stripe; the buyer can’t enter.
- Failed — Stripe declined the charge or the flow broke.
Mobile vs desktop
- Mobile: every order is a vertical card with stacked email and items.
- Desktop (
sm:and up): table with columns, horizontal scroll if it overflows.
Pagination is 25 per page by default. Use Load more / Previous at the bottom.
How these numbers populate
| Event | What happens |
|---|---|
| Buyer pays via Stripe | Order becomes paid, webhook checkout.session.completed fires |
Webhook → fulfillOrder() | Issues QRs, debits 1 credit per ticket (free first), increments tier sold |
Order becomes fulfilled | Shows up in KPIs and the list — the buyer receives the email |
| Refund via Stripe | Webhook marks the order refunded and QRs as refunded (no longer enter) |
| Staff validates QR at the door | The validated count in the QR card goes up |
You don’t need to refresh: React Query refetches every 15s in the background. For a manual pull, reload the browser or switch a filter.