Ticket payment runs through Stripe. The app never holds a Stripe secret; it asks the server for a client secret per purchase and the platform's own Stripe account settles the charge.
Where the publishable key comes from
The key in environment.dart is a placeholder and is not used. At purchase time the app calls GET /client/events/stripe/config for the organization's publishable key, and falls back to the publishableKey returned alongside the payment intent. Configure Stripe on the organization in Studio Manager; nothing changes in the app.
The purchase flow
1. Create the booking. POST /client/events/tickets/purchase with the event, buyer email and name, the ticket lines, an optional promo code and paymentMethod: stripe (or free when the total is zero). The server validates each line against the purchase rules and answers with the booking: pending when payment is due, paid when it is free.
2. Get a client secret. A pending booking normally carries payment.stripeClientSecret. If it does not, the app calls POST /client/events/stripe/intent with the amount in cents, usd, and the booking id as the reference.
3. Present the payment sheet. The Stripe PaymentSheet opens with merchant name "EventOS" and the client secret. The card never touches the app.
4. Confirm the order. POST /client/events/tickets/confirm-order with the booking id, the payment intent id (the client secret before _secret_) and paymentGateway: stripe. The server records the payment, issues the tickets and sends event-ticket-confirmation. Confirming an already-paid booking is rejected, so a retry cannot double-issue.
5. Land on the summary. The booking summary shows the reference, the order, one QR card per ticket, and Assign/Transfer and Share actions.
Purchase is tracked as a ticket_purchase activity on the buyer's customer record.
Free events
A zero total skips steps 2 to 4: the booking is created already paid and tickets are issued immediately. Remember that free ticket types default to one per customer.
Guest checkout
All five purchase routes are public. A buyer without an account gets the confirmation email with the booking reference and can look the booking up later with GET /client/events/booking?email=&bookingId=. Signing up afterwards with the same email makes the tickets appear in My Events automatically, because tickets are matched by holder email.
Other checkout routes
The server also exposes POST /client/events/stripe/checkout-session (hosted Stripe Checkout) and POST /client/events/tickets/register (issue tickets against an existing order). The app uses neither today; they are for web storefronts and integrations.
flutter_stripe is mobile-only. On a web build the payment sheet is a stub that throws, so a paid purchase stops at step 3. Test paid flows on iOS or Android; free registrations work everywhere.
Refunds and cancellations
Attendees cannot refund from the app. Staff refund a ticket or a whole booking from the admin side (POST /events/tickets/:id/refund, POST /events/bookings/:id/refund), which adjusts sold counts and revenue and emails the holder. Cancelling keeps the money and releases the seat; see tickets and bookings.
Currency
Intents are created in usd. Ticket types carry their own currency field for display, but the payment path assumes US dollars today.