ResytechResytech Docs

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)
ClassResytechApiResyeventsApi
Sessioninitialize() + bearer tokenNone — anonymous
Keyed byLocation ID + activity UUIDGlobal event slug (/e/{slug})
CartShopping cartInventory holds (~10 min)
PaymentPlatform chargeDirect 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

  1. Browse — list a venue's events, or fetch one event with its ticket tiers, add-ons, and live availability.
  2. 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.
  3. Check out — the holds are priced into an order and paid by card in the browser.
  4. Confirm — fulfillment happens on a payment webhook, so the order becomes Paid a 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

ComponentTagPurpose
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

ComponentAttributes
<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);
});
EventFired ByDetail
event-selectCard, List{ slug, uuid }
events-loadedList{ events, totalCount }
selection-changeTicket Picker{ quantities, ticketCount, subtotal }
tickets-reservedTicket Picker{ eventUuid, slug, holds }
reserve-failedTicket Picker{ reason, message }
checkout-completeCheckout{ orderUuid, grandTotal, status }
checkout-failedCheckout{ error, message }
holds-expiredCheckout{}
order-settledOrder{ 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>
PropertyDescriptionDefault
--re-bgPage background#ffffff
--re-surfaceCards and panels#ffffff
--re-surface-2Inputs and placeholders#f8fafc
--re-borderBorders and dividers#e2e8f0
--re-fgPrimary text#0f172a
--re-mutedSecondary text#64748b
--re-accentButtons, links, highlights#2563eb
--re-accent-hoverButton hover#1d4ed8
--re-accent-fgText on accent surfaces#ffffff
--re-successConfirmation check#16a34a
--re-dangerErrors, "Only N left"#dc2626
--re-radiusCorner radius12px
--re-fontFont familySystem 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',
});
MemberPurpose
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, subtotalRunning totals
earliestExpiryEpoch ms of the soonest hold expiry, for a countdown
attendeeSlotsOne 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.

CodeMeaning
INSUFFICIENT_INVENTORYSomeone else took them first
EXCEEDS_MAX_PER_ORDEROver the tier's per-order limit
SESSION_HOLD_LIMITThis cart already holds as much as one cart may
NOT_ON_SALEOutside the sale window
TIER_INACTIVEThe organizer deactivated the tier
INVALID_SESSION_TOKENSession expired or rejected — mint a new one
NO_ACTIVE_HOLDSThe holds lapsed before checkout
ORGANIZER_NOT_READYStripe onboarding incomplete — hide the buy flow
CAPTCHA_FAILEDTurnstile challenge not passed
ATTENDEE_NAMES_REQUIREDThe event needs a name per ticket
MAX_PER_EMAILThis email hit the event's per-buyer limit
PAYMENT_SETUP_FAILEDThe 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',
});
OptionDefaultDescription
baseUrlhttps://api.resyevents.com/v1Ticketing API host
debugfalseConsole logging
timeout30000Request timeout in ms
headersCustom headers on every request
cartStorageKeyresyevents_cartsessionStorage 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.

AttributeRequiredDescription
data-resyevents-autoYesSet to "true" to auto-initialize
data-resyevents-debugNoSet to "true" for console logging
data-resyevents-base-urlNoOverride 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: false means 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

On this page