Skip to content
Docs
Quickstart

Next.js quickstart

Server components read the catalogue through @ticketlayer/live/server; a client provider boots the browser SDK and Elements for the buy flow.

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

:::note The same shape the hosted box office uses These three packages are on npm, and the code below is what the hosted box office - itself a Next.js app - does. To try an unreleased build, check out ticketlayer/live (pnpm install && pnpm build) and link the packages in. :::

Environment#

.env.local
LIVE_API_INTERNAL_URL=https://live.ticketlayer.com
NEXT_PUBLIC_LIVE_API_URL=https://live.ticketlayer.com
NEXT_PUBLIC_CHECKOUT_BASE=https://checkout.ticketlayer.com
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