Resyevents Ticketing
Sell tickets to live events on your own website with resyevents.js — a separate library from the booking client.
resyevents.js sells tickets to live events on your own site: browse a venue's events, reserve tickets, take payment, and confirm the order. Everything the Resyevents storefront does, on your domain.
This is a separate library from resytech.js. It has its own bundle, its own class, and shares no code with the booking client — so a ticketing page never downloads the booking library, and the two can sit on the same page without colliding.
Quick Start
<script src="https://js.resytech.com/latest/resyevents.js"></script>
<resyevents-ticket-picker
event-slug="summer-block-party"
checkout-url="/tickets/checkout">
</resyevents-ticket-picker>
<script>
const events = new ResyeventsApi();
events.registerComponents();
</script>There is no initialize() call and no access token. Every endpoint on the ticketing API is anonymous — the event slug is all you need.
Events Are Not Bookings
Live events are a different product on a different host, and nothing from ResytechApi applies:
Booking (resytech.js) | Ticketing (resyevents.js) | |
|---|---|---|
| Class | ResytechApi | ResyeventsApi |
| Session | initialize() + bearer token | None — anonymous |
| Keyed by | Location ID + activity UUID | Global event slug (/e/{slug}) |
| Cart | Shopping cart | Inventory holds (~10 min) |
| Payment | Platform charge | Direct charge on the organizer's Stripe account |
You can load both libraries on one page. They define separate globals and share no symbols.
The Buying Flow
- Browse — list a venue's events, or fetch one event with its ticket tiers, add-ons, and live availability.
- Reserve — the buyer's picks become server-side holds that take that inventory out of circulation for about 10 minutes, so two buyers racing for the last tickets can never both win.
- Check out — the holds are priced into an order and paid by card in the browser.
- Confirm — fulfillment happens on a payment webhook, so the order becomes
Paida moment after the card clears. Poll for it. The tickets themselves are emailed to the buyer.
A successful card payment means paid, not ticketed. Never tell a buyer their tickets exist based on the checkout response alone — poll the order until it settles, or send them to a page that does.
Web Components
The fastest path. Three pages, or all on one — the cart survives navigation via sessionStorage.
<!-- A venue's upcoming events -->
<resyevents-list
venue-slug="the-warehouse"
limit="12"
href-pattern="/tickets/{slug}">
</resyevents-list>
<!-- Pick tickets, then send the buyer to your checkout page -->
<resyevents-ticket-picker
event-slug="summer-block-party"
checkout-url="/tickets/checkout">
</resyevents-ticket-picker>
<!-- Buyer details, payment, order creation -->
<resyevents-checkout
return-url="/tickets/confirmed"
back-url="/tickets/summer-block-party">
</resyevents-checkout>
<!-- Polls until the order settles, then shows the confirmation -->
<resyevents-order></resyevents-order><resyevents-order> reads ?order= from the URL, which is exactly what <resyevents-checkout return-url="…"> appends — so those two need no wiring between them.
Components
| Component | Tag | Purpose |
|---|---|---|
| Event Card | <resyevents-card> | One event as a card |
| Event List | <resyevents-list> | A venue's events, paginated |
| Ticket Picker | <resyevents-ticket-picker> | Tiers, add-ons, and the reserve action |
| Checkout | <resyevents-checkout> | Buyer details, Stripe payment, order creation |
| Order | <resyevents-order> | Confirmation, polling until the order settles |
Attributes
| Component | Attributes |
|---|---|
<resyevents-card> | event-slug, href-pattern, show-venue, show-price |
<resyevents-list> | venue-slug, limit, past, href-pattern, show-title, title-text, empty-text |
<resyevents-ticket-picker> | event-slug, checkout-url, show-event-header, cta-text |
<resyevents-checkout> | return-url, back-url, event-slug |
<resyevents-order> | order-uuid, query-param |
href-pattern takes {slug} and {uuid} placeholders and turns each card into a real link. Leave it off and the card emits event-select instead.
Events
document.addEventListener('tickets-reserved', (e) => {
console.log('Held until', e.detail.holds[0].expiresAt);
});| Event | Fired By | Detail |
|---|---|---|
event-select | Card, List | { slug, uuid } |
events-loaded | List | { events, totalCount } |
selection-change | Ticket Picker | { quantities, ticketCount, subtotal } |
tickets-reserved | Ticket Picker | { eventUuid, slug, holds } |
reserve-failed | Ticket Picker | { reason, message } |
checkout-complete | Checkout | { orderUuid, grandTotal, status } |
checkout-failed | Checkout | { error, message } |
holds-expired | Checkout | {} |
order-settled | Order | { outcome, order } |
Theming
Components use the event's own theme when the organizer set one, falling back to a neutral light palette — so an unthemed event does not drop a dark panel onto your page. Override with CSS custom properties:
<resyevents-ticket-picker
event-slug="summer-block-party"
style="--re-accent: #e11d48; --re-radius: 8px;">
</resyevents-ticket-picker>| Property | Description | Default |
|---|---|---|
--re-bg | Page background | #ffffff |
--re-surface | Cards and panels | #ffffff |
--re-surface-2 | Inputs and placeholders | #f8fafc |
--re-border | Borders and dividers | #e2e8f0 |
--re-fg | Primary text | #0f172a |
--re-muted | Secondary text | #64748b |
--re-accent | Buttons, links, highlights | #2563eb |
--re-accent-hover | Button hover | #1d4ed8 |
--re-accent-fg | Text on accent surfaces | #ffffff |
--re-success | Confirmation check | #16a34a |
--re-danger | Errors, "Only N left" | #dc2626 |
--re-radius | Corner radius | 12px |
--re-font | Font family | System stack |
Each component also exposes part names (container, card, row, summary, cta, totals, tickets) for ::part() styling.
Building Your Own UI
events.cart holds the session and hold protocol so you do not have to reimplement it.
const events = new ResyeventsApi();
const { event } = await events.event.getEvent('summer-block-party');
events.cart.begin(event.uuid, event.slug);
// All-or-nothing: if any hold fails, the ones already taken are released for you.
const result = await events.cart.reserve([
{ kind: 'ticket', uuid: tier.uuid, name: tier.name, price: tier.price, quantity: 2 },
{ kind: 'addon', uuid: addon.uuid, name: addon.name, price: addon.price, quantity: 1 },
]);
if (!result.success) {
showError(result.message); // already readable: 'Only 2 left for "VIP".'
return;
}
const quote = await events.cart.quote(); // real fees and taxes
const checkout = await events.cart.checkout({
buyerName: 'Alex Chen',
buyerEmail: 'alex@example.com',
});| Member | Purpose |
|---|---|
begin(uuid, slug) | Point the cart at an event, discarding a different one |
reserve(selections) | Create holds, all-or-nothing |
quote() | Price the holds — real fees and taxes |
checkout(details) | Turn the holds into a payable order |
releaseAll() | Free the holds (a "back to tickets" action) |
ticketCount, subtotal | Running totals |
earliestExpiry | Epoch ms of the soonest hold expiry, for a countdown |
attendeeSlots | One label per ticket, in the order names must be supplied |
hasActiveHoldsFor(uuid) | Whether a checkout page has something to check out |
Taking Payment
Event charges are direct charges on the organizer's connected Stripe account, so Stripe.js must be scoped to it:
if (checkout.success && checkout.status === 'paid') {
// A free order — already fulfilled, no payment step.
} else if (checkout.success) {
const stripe = Stripe(publishableKey, { stripeAccount: checkout.stripeAccountId });
const elements = stripe.elements({ clientSecret: checkout.clientSecret });
// …mount the Payment Element and confirm
}Use checkout.isTestMode to pick which publishable key to load. Pairing a live key with a test-mode connected account makes Stripe() fail and the Payment Element never mounts.
Rules the API Enforces
Each of these fails quietly if you ignore it. events.cart handles all of them — you only need them when calling events.holds directly.
Mint a session before holding anything. events.holds.createSession(). The server does not create one implicitly; a request without a token used to get a brand-new session every time, which defeated the per-session hold limit.
Store the sessionToken from every response that returns one. The server refreshes it once it is past half its life, to keep an active buyer from lapsing. Ignoring the refreshed value works right up until the original expires — at which point the cart dies mid-checkout with live holds still holding inventory.
Drop a rejected session, do not retry it. INVALID_SESSION_TOKEN is reported separately from INVALID_REQUEST precisely so you can tell "the session is dead" from "the email is malformed".
Turnstile tokens are single use. Reset the widget after every submit attempt. A challenge only applies to events with a free tier; the site key arrives on the event as captchaSiteKey, and can also arrive later on a CAPTCHA_FAILED response if the gate armed mid-session.
Quantity Rules
The picker enforces the same limits as the Resyevents storefront, so an embedded picker and the public page behave identically for the same event:
- A tier with a minimum per order must be bought at or above that minimum.
- The stepper is capped by whichever is lower: the organizer's per-order maximum, or what is actually left. With no maximum set, one order tops out at 10 of that tier.
- At 8 or fewer remaining, an "Only N left" line appears.
- Add-on steppers stay disabled until at least one ticket is selected — nobody can buy a drink package with no admission.
Availability is a snapshot: another buyer can take the last ticket between the page load and the hold. Show available as guidance and let reserve() settle it — on failure the picker re-reads the event so the buyer sees what is truly left.
Failure Codes
Failures come back with a machine-readable code and readable text already in message. describeHoldFailure() and describeCheckoutFailure() are exported if you want your own wording.
| Code | Meaning |
|---|---|
INSUFFICIENT_INVENTORY | Someone else took them first |
EXCEEDS_MAX_PER_ORDER | Over the tier's per-order limit |
SESSION_HOLD_LIMIT | This cart already holds as much as one cart may |
NOT_ON_SALE | Outside the sale window |
TIER_INACTIVE | The organizer deactivated the tier |
INVALID_SESSION_TOKEN | Session expired or rejected — mint a new one |
NO_ACTIVE_HOLDS | The holds lapsed before checkout |
ORGANIZER_NOT_READY | Stripe onboarding incomplete — hide the buy flow |
CAPTCHA_FAILED | Turnstile challenge not passed |
ATTENDEE_NAMES_REQUIRED | The event needs a name per ticket |
MAX_PER_EMAIL | This email hit the event's per-buyer limit |
PAYMENT_SETUP_FAILED | The charge could not be opened; message has Stripe's reason |
Dates and Time Zones
Timestamps are ISO strings, not Date objects, and every event carries its own timeZone — which is not the browser's. A doors-open time must read as the local time at the venue no matter where the buyer is sitting.
import { formatRange, formatDay, formatTime, asUtcDate } from 'resytech-client-lib/resyevents';
formatRange(event.startAt, event.endAt, event.timeZone);
// "Sat, Sep 12, 2026 · 7:00 PM – 11:00 PM EDT"Formatting with plain new Date(...).toLocaleString() shifts every event by the viewer's offset.
API Reference
// Discovery
await events.event.getEvent(slug);
await events.venue.getVenue(slug);
await events.venue.getEvents(slug, { past: false, limit: 12, offset: 0 });
// Cart — the intended entry point
events.cart.begin(eventUuid, eventSlug);
await events.cart.reserve(selections);
await events.cart.quote();
await events.cart.checkout({ buyerEmail, buyerName, attendeeNames, captchaToken });
await events.cart.releaseAll();
// Raw endpoints — escape hatch
await events.holds.createSession();
await events.holds.holdTickets({ ticketTypeUuid, quantity, sessionToken });
await events.holds.holdAddon({ addonUuid, quantity, sessionToken });
await events.holds.releaseHold(holdUuid, sessionToken);
await events.checkout.quote({ eventUuid, sessionToken });
await events.checkout.checkout({ eventUuid, sessionToken, buyerEmail });
// Order
await events.order.getOrder(orderUuid);
await events.order.pollUntilSettled(orderUuid, { intervalMs, maxAttempts, onUpdate, signal });Every response carries the same envelope as the booking client — success, message, statusCode, action, isNetworkError — plus its payload. Nothing throws: a network failure comes back as success: false with isNetworkError: true. See Error Handling.
Configuration
const events = new ResyeventsApi({
debug: true,
timeout: 30000,
cartStorageKey: 'resyevents_cart',
});| Option | Default | Description |
|---|---|---|
baseUrl | https://api.resyevents.com/v1 | Ticketing API host |
debug | false | Console logging |
timeout | 30000 | Request timeout in ms |
headers | — | Custom headers on every request |
cartStorageKey | resyevents_cart | sessionStorage key; change it to run two carts on one page |
Auto-Initialization
<script src="https://js.resytech.com/latest/resyevents.js" data-resyevents-auto="true"></script>Creates the client, registers the components, and exposes it as window.resyevents. Without the attribute nothing is created and no request is made.
| Attribute | Required | Description |
|---|---|---|
data-resyevents-auto | Yes | Set to "true" to auto-initialize |
data-resyevents-debug | No | Set to "true" for console logging |
data-resyevents-base-url | No | Override the API host |
Security Notes
- An order UUID is a bearer credential. It grants access to that order — buyer details and the admission tokens. Keep it out of anything indexable, cacheable, or shareable, and off analytics query strings.
- Admission QR codes are emailed, never rendered by these components. That is deliberate; a confirmation page is a far easier thing to screenshot and forward than an inbox.
chargesEnabled: falsemeans the organizer has not finished Stripe onboarding. Hide the buy flow rather than letting the buyer fail at the last step — the ticket picker does this for you.
Next Steps
- Live Events (operator guide) — creating events, tiers, venues, payouts
- The Storefront — what the public page does, which these components mirror
- Error Handling — the response envelope
