Skip to main content

Next.js quickstart

Two halves. On the server, @ticketlayer/live/server reads listings, event detail and the theme with plain fetch, no DOM, no session, so your pages render with real data and cache however Next.js caches. In the browser, @ticketlayer/live plus the Elements handle the buy flow, the cart and the hosted checkout.

Install

npm install @ticketlayer/live @ticketlayer/elements @ticketlayer/elements-react
Not on npm yet

These three packages publish once their release workflows run; today the install fails. The code below is the intended shape (it is what the hosted box office, itself a Next.js app, does). To try it before the packages publish, depend on the repositories directly ("@ticketlayer/live": "github:ticketlayer/live-sdk" and build them), or use the HTML quickstart script tag in app/layout.tsx and plain <tl-...> tags in your JSX.

Environment

.env.local
LIVE_API_INTERNAL_URL=https://live.staging.t9r.dev
NEXT_PUBLIC_LIVE_API_URL=https://live.staging.t9r.dev
NEXT_PUBLIC_CHECKOUT_BASE=https://hbo.staging.t9r.dev
LIVE_PUBLISHABLE_KEY=tlpk_...
NEXT_PUBLIC_LIVE_PUBLISHABLE_KEY=tlpk_...

The publishable key is public by design, so it can be in both a server and a NEXT_PUBLIC_ variable. Never put an organisation API key (tlak_) in a NEXT_PUBLIC_ variable.

Server: the catalogue client

lib/live.ts
import { createServerClient } from '@ticketlayer/live/server';

export const live = createServerClient({
apiUrl: process.env.LIVE_API_INTERNAL_URL!,
publishableKey: process.env.LIVE_PUBLISHABLE_KEY!,
// Let Next's fetch cache own revalidation instead of the SDK's TTL cache.
cache: false,
});
app/page.tsx
import Link from 'next/link';
import { live } from '@/lib/live';

export default async function HomePage() {
const listings = await live.events.list({
fetchInit: { next: { revalidate: 300, tags: ['events'] } },
});
return (
<main>
<h1>What's on</h1>
<ul>
{listings.map((l) => (
<li key={l.id}>
<Link href={`/events/${l.eventId}`}>{l.name}</Link>
</li>
))}
</ul>
</main>
);
}
app/events/[id]/page.tsx
import { notFound } from 'next/navigation';
import { LiveAPIError } from '@ticketlayer/live/server';
import { live } from '@/lib/live';
import { BuyButton } from '@/components/BuyButton';

export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const [event, theme] = await Promise.all([
live.events.get(id, { fetchInit: { next: { revalidate: 300, tags: ['events', `event:${id}`] } } }),
live.theme.get({ fetchInit: { next: { revalidate: 300, tags: ['theme'] } } }),
]);
return (
<main style={{ fontFamily: theme.typography?.fontFamily }}>
<h1>{event.name}</h1>
<BuyButton eventId={id} />
</main>
);
} catch (err) {
if (err instanceof LiveAPIError && err.status === 404) notFound();
throw err;
}
}

The server client's surface: events.list(), events.get(id), theme.get(), session.create() (a Live session, for a WebView checkout), get(path) for any other Live GET, and clearCache(). Every call takes { fetchInit } to pass framework fetch options through.

Browser: boot the SDK and register Elements once

components/TicketlayerProvider.tsx
'use client';
import { useEffect } from 'react';
import { createLiveClient } from '@ticketlayer/live';
import { defineCustomElements } from '@ticketlayer/elements-react';

export function TicketlayerProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Register the custom elements after hydration (avoids SSR mismatches).
defineCustomElements();
createLiveClient({
baseUrl: process.env.NEXT_PUBLIC_LIVE_API_URL!,
publishableKey: process.env.NEXT_PUBLIC_LIVE_PUBLISHABLE_KEY!,
// Third-party pages open the hosted checkout in an iframe modal.
checkout: { mode: 'modal', embedBaseUrl: process.env.NEXT_PUBLIC_CHECKOUT_BASE! },
}).then((tl) => {
tl.on('checkout:completed', (order: { id: string }) => {
window.location.href = `/thanks?orderId=${encodeURIComponent(order.id)}`;
});
});
}, []);
return <>{children}</>;
}

Mount it in app/layout.tsx around {children}. createLiveClient publishes the client on window.Ticketlayer, which is how every Element finds it; there is no prop to pass.

components/BuyButton.tsx
'use client';
import { TlBuyTicketsButton } from '@ticketlayer/elements-react';

export function BuyButton({ eventId }: { eventId: string }) {
return <TlBuyTicketsButton eventId={eventId} size="lg" onTlClick={() => console.log('opening buy flow')} />;
}
app/thanks/page.tsx
'use client';
import { TlOrderConfirmation } from '@ticketlayer/elements-react';

// <tl-order-confirmation> reads ?orderId= from the URL itself.
export default function ThanksPage() {
return <TlOrderConfirmation />;
}

Elements are browser-only: every component that renders one needs 'use client'. Server components should render data from the server client and hand off to a client component for anything interactive.

Next

  • Elements reference: TlEventList, TlCartBadge, TlCartDrawer, TlMyOrders and the rest
  • Live SDK reference for createLiveClient options and the TicketlayerLive facade
  • Webhooks to update your own database when an order confirms