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
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
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
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,
});
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>
);
}
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
'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.
'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')} />;
}
'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,TlMyOrdersand the rest - Live SDK reference for
createLiveClientoptions and theTicketlayerLivefacade - Webhooks to update your own database when an order confirms