Skip to main content

Pricing and estimates

How Rift prices ticket types across waves, what the SDK shows the attendee at each step, and the disclaimer organizers are required to surface.

The wave-tier model

Organizers configure events with one or more ticket types (General admission, VIP, Backstage). The same ticket type can be priced differently across waves — pricing/scheduling tiers the organizer uses to reward early commitment or to throttle demand. Typical shape:

WaveGA priceVIP priceWindow
Early bird$80$200Until 2 weeks before
Regular$100$250Until day of
At the door$120$300Day-of only

Waves overlap when the organizer wants them to (e.g. early bird remaining capacity continues to sell at $80 alongside the regular-priced wave at $100).

The attendee never picks a wave. The reservation service walks waves cheapest-first when fulfilling, so a { ticketTypeId, quantity } request gets the best available price automatically. Wave names exist server-side for the breakdown the SDK shows in the estimate; they are not a selection concept.

What the SDK shows where

The SDK collapses waves at every UI seam so the picker stays a flat list of ticket types and the price breakdown surfaces only when it matters:

SurfaceWave concept visible?
<AvailabilityList>No. One row per ticket type. Cheapest active wave's price shown as "From $X".
<TicketRow>No. Aggregated price + stepper clamped at sum(available) and the most permissive maxPerOrder across active waves with inventory.
<EstimateBreakdown> (pre)Yes — the breakdown is the whole point. Hierarchical: ticket type → split lines per consumed wave → subtotal → grand total + disclaimer.
<ReservationSummary> (post)Yes — same shape, server-confirmed data. Wave names resolved from the cached availability tree.

The greedy-fill algorithm

useSelectionEstimate mirrors what the reservation service does server-side. For each ticket type with quantity > 0:

  1. Order its currently-active waves by unitPrice ascending (cheapest first). useTicketTypes does this aggregation.
  2. Walk: take = min(remaining, wave.available); record a line { waveId, quantity: take, unitPrice }; subtract take from remaining.
  3. Stop when remaining hits 0 (the stepper clamping prevents exhausting waves before satisfying remaining).

The pricing nuance worth internalizing: unit price × quantity is wrong when quantity exceeds the cheapest wave's remaining.

Example: GA has 1 left at Early bird ($80), unlimited at Regular ($100). Attendee wants 2 → pays $80 + $100 = $180, not $80 × 2 = $160. The greedy fill produces the right number; the breakdown shows them where the math came from.

Active-wave filtering

The Experiences API returns every wave on an event with its startsAt/endsAt windows, regardless of whether it's currently selling. useTicketTypes filters to active waves (startsAt <= getNow() <= endsAt) before aggregating — so expired waves' stale prices never enter the estimate and not-yet- live waves don't get sold against.

For test and Storybook scenarios, pass a frozen clock to control which waves are considered active:

<EstimateBreakdown now={() => new Date("2026-07-01T12:00:00Z")} />
<AvailabilityList now={() => new Date("2026-07-01T12:00:00Z")} />

The disclaimer

<EstimateBreakdown> and <ReservationSummary> ship a default-visible disclaimer near the total. Two reasons:

  1. Cheapest-first tier selection. Attendees benefit from knowing the algorithm — it's why a $80 ticket can show up alongside a $100 ticket on the same line item.
  2. Modifier transparency. Server-side modifiers (platform fees, taxes, discounts) are computed in the pricing engine and only appear in the reservation/checkout response's breakdown.steps. The estimate shows line items only; the final total can differ. Pricing-transparency rules in many jurisdictions require this kind of "fees added at checkout" call-out.

Default copy (en):

"We pick the cheapest available tier first. Final price (including any fees) is confirmed when you reserve."

Override via the disclaimer prop on either component, or globally via <RiftProvider strings={{ "estimate.disclaimer": "…" }}>.

disclaimer={null} hides it entirely. Don't unless you display equivalent copy elsewhere on the page — the default is the regulatory-friendly path.

What's NOT in the estimate

The client-side estimate covers line items: (ticketType, wave, quantity, unitPrice). It does NOT cover:

  • Platform fees (a percentage modifier the platform adds).
  • Per-order fixed fees (booking fees, processing fees).
  • Taxes (region-specific, organizer-configurable).
  • Discount codes (haven't been applied at estimate time).

Those live in the reservation response's breakdown.steps. When the server's total differs from the client's estimate, useReservation().priceChanged flips true and <PriceChangedWarning> surfaces the delta. The disclaimer prepares the attendee for that possibility before they commit.

Bypassing the components

Direct hook access for custom UIs:

import { useSelectionEstimate, useTicketTypes } from "@feelrift/react";

function CustomCart() {
const { data: ticketTypes } = useTicketTypes();
const { data: estimate } = useSelectionEstimate();

if (!estimate) return null;

return (
<div>
<h2>{estimate.ticketTypes.length} ticket types selected</h2>
<strong>{formatCurrency(estimate.subtotal)}</strong>
{/* Render lines / disclaimer however you like */}
</div>
);
}

The pure functions are exported too for unit testing or non-React contexts:

  • aggregateTicketTypes(availability, now) — the wave-collapse.
  • computeEstimate(ticketTypes, selection) — the greedy-fill.

Both are deterministic, side-effect-free, and tested in isolation on every SDK release.