Next.js

Add the chat widget to a Next.js App Router app, with a token route for signed-in users.

This guide uses the App Router. You’ll add a client provider, a token route, and one environment variable.

Install

npm install @ticketping/chat-widget@next

v2 is on the next tag until 2.0.0 is released.

Add the provider

Create a client component that wraps your app. Get your publishable key (pk_...) from Settings → Widgets. It’s public, so it can live in your code.

app/providers.tsx:

tsx
'use client'

import { TicketpingProvider } from '@ticketping/chat-widget/react'

type User = { id: string; email?: string | null; name?: string | null }

async function getToken() {
  const res = await fetch('/api/ticketping-token', { method: 'POST' })
  if (!res.ok) throw new Error(`Ticketping token request failed (${res.status})`)
  return res.text()
}

export function Providers({ user, children }: { user: User | null; children: React.ReactNode }) {
  return (
    <TicketpingProvider publishableKey="pk_..." user={user} getToken={getToken}>
      {children}
    </TicketpingProvider>
  )
}

Render it in your root layout and pass the signed-in user, or null. This example uses Auth.js; use whatever your app already has.

app/layout.tsx:

tsx
import { auth } from '@/auth'
import { Providers } from './providers'

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const session = await auth()
  const user = session?.user?.id
    ? { id: session.user.id, email: session.user.email, name: session.user.name }
    : null

  return (
    <html lang="en">
      <body>
        <Providers user={user}>{children}</Providers>
      </body>
    </html>
  )
}

The provider follows the user prop: it identifies the user on sign-in, switches cleanly when a different user signs in, and calls logout when user becomes null. Without a user, the widget runs for anonymous visitors.

The React adapter ships with 2.0. If a prop name here doesn’t match your installed beta, check the package README; the core JavaScript API works the same in every version.

Add the token route

The widget calls getToken when it needs to prove who the user is. This route signs a token for the signed-in user. It’s the only server code you write.

npm install jose

app/api/ticketping-token/route.ts:

ts
import { SignJWT } from 'jose'
import { auth } from '@/auth'

const secret = new TextEncoder().encode(process.env.TICKETPING_IDENTITY_SECRET)

export async function POST() {
  const session = await auth()
  const user = session?.user
  if (!user?.id) return new Response('Unauthorized', { status: 401 })

  const profile: Record<string, string> = {}
  if (user.email) profile.email = user.email
  if (user.name) profile.name = user.name

  const token = await new SignJWT(profile)
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(String(user.id))
    .setExpirationTime('5m')
    .sign(secret)

  return new Response(token, { headers: { 'Content-Type': 'text/plain' } })
}

Using Clerk or another library? Replace the first lines with its way of reading the signed-in user, for example const { userId } = await auth() from @clerk/nextjs/server.

Set the secret

Get the identity secret from Settings → API → Identity secret. It’s shown once.

.env.local:

bash
TICKETPING_IDENTITY_SECRET=tpis_...

Use the same value in every environment: add it to your host’s environment variables (for example Vercel → Settings → Environment Variables) for preview and production.

Never prefix it with NEXT_PUBLIC_. That would ship it to the browser.

Check that it works

  1. Run npm run dev, sign in, and open the browser console.
  2. Run await (await fetch('/api/ticketping-token', { method: 'POST' })).text() and copy the token.
  3. Paste it into the token validator under Settings → API → Identity secret. It shows the claims, or says exactly what to fix.
  4. Reload and send a message from the widget. In the dashboard it shows the user’s name with a Verified badge and a TEST tag, because it came from localhost.

Use the widget from your components

tsx
'use client'

import { useTicketping } from '@ticketping/chat-widget/react'

export function HelpButton() {
  const { unreadCount, open, showNewMessage } = useTicketping()

  return (
    <>
      <button onClick={open}>Help{unreadCount > 0 ? ` (${unreadCount})` : ''}</button>
      <button onClick={() => showNewMessage('I have a question about billing')}>
        Ask about billing
      </button>
    </>
  )
}

useTicketping() returns unreadCount and isOpen, which update live, plus open(), close(), toggle(), showNewMessage(), showConversation(), showSpace(), update(), setContext() and trackEvent(). Other methods, such as logout and on, are on client from the same hook. Importing Ticketping from @ticketping/chat-widget is a different instance and does not control this provider.

user may use id or userId. Other provider props (appearance, texts, features, locale, hideLauncher, consent) are the same as the init options in Configuration. Those options are read once; remount the provider to change them, or call update().

Copy prompt for your AI coding agent

prompt
Add the Ticketping chat widget (v2) to this Next.js App Router app.

Docs index: https://ticketping.com/llms.txt
This page as Markdown: https://ticketping.com/docs/react-nextjs.md

1. Install @ticketping/chat-widget@next and jose.
2. Create app/providers.tsx ('use client') that renders
   <TicketpingProvider publishableKey="pk_..." user={user} getToken={getToken}> from '@ticketping/chat-widget/react'.
   getToken POSTs to /api/ticketping-token and returns res.text(); throw if !res.ok.
   Ask me for the publishable key (starts with pk_). It's public.
3. In app/layout.tsx, read the signed-in user with this app's existing auth (server side) and pass
   { id, email, name } or null to <Providers user={...}>.
4. Create app/api/ticketping-token/route.ts with a POST handler that returns 401 when nobody is signed in,
   and otherwise returns, as text/plain, a JWT made with jose:
   new SignJWT({ email, name }).setProtectedHeader({ alg: 'HS256' }).setSubject(String(user.id)).setExpirationTime('5m')
   signed with new TextEncoder().encode(process.env.TICKETPING_IDENTITY_SECRET).
   Use the code on the docs page above and adapt only the user lookup.
5. Add TICKETPING_IDENTITY_SECRET= to .env.local (not committed) and .env.example. Never use a NEXT_PUBLIC_ prefix.
6. Don't cache tokens; the widget asks for a fresh one when it needs it.
7. Tell me how to verify: sign in on localhost, run await (await fetch('/api/ticketping-token', { method: 'POST' })).text()
   in the console, and paste the token into the token validator at Settings → API → Identity secret.