SvelteKit
Add the chat widget to a SvelteKit app with Svelte 5, with a +server.ts route for the token.
You’ll add one component to your root layout and one +server.ts route. The adapter needs Svelte 5.57.1 or newer.
Install
npm install @ticketping/chat-widget@2.0.0-beta.12 josev2 is in beta until 2.0.0 is released. Pin the exact version, as above: the next tag moves with every beta. To see the newest beta, run npm view @ticketping/chat-widget dist-tags.next.
The beta adapter is compiled with Svelte 5.57. With an older Svelte, vite build fails with "only_child" is not exported by ".../svelte/src/internal/client/index.js". Upgrade first: npm install -D svelte@^5.57.1.
Add the component
Pass the signed-in user from your root layout’s server load. Most apps set locals.user in hooks.server.ts; use whatever yours already has.
src/routes/+layout.server.ts:
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = ({ locals }) => ({
user: locals.user
? { id: String(locals.user.id), email: locals.user.email, name: locals.user.name }
: null
})Get your publishable key (pk_...) from Settings → Widgets. It’s public.
src/routes/+layout.svelte:
<script lang="ts">
import { onMount } from 'svelte'
import { Ticketping } from '@ticketping/chat-widget/svelte'
let { data, children } = $props()
// The adapter is browser-only. Rendering it during SSR throws effect_orphan (a 500 on every page).
let mounted = $state(false)
onMount(() => {
mounted = true
})
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()
}
</script>
{#if mounted}
<Ticketping publishableKey="pk_..." user={data.user} {getToken} />
{/if}
{@render children()}The component follows the user prop: it identifies on sign-in, switches users cleanly, and logs out when user becomes null (for example after your sign-out action invalidates the layout). undefined means “still loading” and does nothing. null before anyone was identified is also harmless.
Keep the {#if mounted} guard. The beta’s Svelte entry is compiled for the browser, so SvelteKit can’t render it on the server. vite build still passes without the guard; the 500 only shows up when a page is requested. Check with npm run preview and load a page before you deploy.
The Svelte 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
src/routes/api/ticketping-token/+server.ts:
import { env } from '$env/dynamic/private'
import { SignJWT } from 'jose'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ locals }) => {
const user = locals.user
if (!user) 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(new TextEncoder().encode(env.TICKETPING_IDENTITY_SECRET))
return new Response(token, { headers: { 'Content-Type': 'text/plain' } })
}locals.user is whatever your hooks.server.ts sets. Type it in src/app.d.ts as usual.
If your API runs on another origin
Many SvelteKit apps call a separate backend (for example api.example.com running Django or Rails) with a token instead of a session cookie. Then sign the token in that backend, not in SvelteKit. Skip the +server.ts route, and copy the backend route from Identity verification (for DRF, see Django).
import { PUBLIC_API_URL } from '$env/static/public'
async function getToken() {
const res = await fetch(`${PUBLIC_API_URL}/api/ticketping-token/`, {
method: 'POST',
// Send whatever your API already accepts, for example `Token <key>` or `Bearer <jwt>`.
headers: { Authorization: `Token ${apiToken}` }
})
if (!res.ok) throw new Error(`Ticketping token request failed (${res.status})`)
return res.text()
}- Use a plain
fetchwith no body. Shared API helpers often addContent-Type: application/jsonand a JSON body, which the route doesn’t need. - The backend must return the JWT as
text/plain. A JSON string ("eyJ..."with quotes) is rejected. In DRF, returnHttpResponse(token, content_type="text/plain"), notResponse(token). - The backend’s CORS settings must allow your site’s origin,
POSTand theAuthorizationheader. Token auth needs no CSRF header. - If the signed-in user lives in a client store rather than layout data, pass
user={$auth.id ? { id: $auth.id, email: $auth.email } : null}. Your sign-out code must reset that store, which logs the widget out.
Set the secret
Get the identity secret from Settings → API → Identity secret.
.env:
TICKETPING_IDENTITY_SECRET=tpis_...The route reads it with $env/dynamic/private, so SvelteKit never lets it into client code. Set the same value in your host’s environment for every deployment.
Check that it works
- Run
npm run build && npm run previewand load a few pages. They must return 200, not 500. - Run
npm run dev, sign in, and open the browser console. - Run
await (await fetch('/api/ticketping-token', { method: 'POST' })).text()and copy the token. - Paste it into the token validator under Settings → API → Identity secret. It shows the claims, or says exactly what to fix.
- Reload and send a message. The dashboard shows it with the user’s name, a Verified badge and a TEST tag.
Use the widget from your components
user may be { id, email, name } or { userId, email, name } (strings or numbers). Other props (appearance, texts, features, locale, hideLauncher, consent) match the init options in Configuration.
Open it from your own buttons
Create the instance yourself and hand it to the component. Then any component can open the widget, start a conversation with a prefilled message, or call the rest of the JavaScript API.
src/lib/ticketping.ts:
import { createTicketping } from '@ticketping/chat-widget/svelte'
// Safe to create during SSR: nothing touches the page until <Ticketping> mounts.
export const ticketping = createTicketping()src/routes/+layout.svelte:
<Ticketping client={ticketping} publishableKey="pk_..." user={data.user} {getToken} />Anywhere else:
<script lang="ts">
import { ticketping } from '$lib/ticketping'
</script>
<button onclick={() => ticketping.showNewMessage('Question about order 1234')}>Contact support</button>Calls made before the widget has finished loading are kept: it opens with your message once it’s ready. The component still owns the lifecycle: it calls init on mount and destroy on unmount.
Read its state
Components rendered inside <Ticketping> can call useTicketping(). It returns isOpen and unreadCount (reactive) plus every method.
<!-- +layout.svelte -->
<Ticketping client={ticketping} publishableKey="pk_..." user={data.user} {getToken}>
{@render children()}
</Ticketping><script lang="ts">
import { useTicketping } from '@ticketping/chat-widget/svelte'
const ticketping = useTicketping()
</script>
<button onclick={() => ticketping.toggle()}>
Help {#if ticketping.unreadCount}({ticketping.unreadCount}){/if}
</button>Ticketping imported from @ticketping/chat-widget (the plain npm entry) is a separate instance and does not control the component; use createTicketping() from the Svelte entry as above.
Copy prompt for your AI coding agent
Add the Ticketping chat widget (v2) to this SvelteKit app (Svelte 5).
Docs index: https://ticketping.com/llms.txt
This page as Markdown: https://ticketping.com/docs/svelte.md
1. Install the exact beta: @ticketping/chat-widget@2.0.0-beta.12 (no ^, not @next) and jose.
If package.json has svelte older than 5.57.1, upgrade it first (the adapter is compiled with 5.57;
older versions fail vite build with "only_child" is not exported).
2. In src/routes/+layout.server.ts, return user: { id: String(id), email, name } for the signed-in user from
locals (or this app's existing session code), or null.
3. In src/routes/+layout.svelte, render
<Ticketping publishableKey="pk_..." user={data.user} {getToken} /> from '@ticketping/chat-widget/svelte'
before {@render children()}, inside {#if mounted} where mounted is set to true in onMount.
Do not drop the guard: the adapter is browser-only and SSR returns 500 (effect_orphan) without it.
getToken POSTs to /api/ticketping-token and returns res.text(); throw if !res.ok. Never cache the token.
Ask me for the publishable key (starts with pk_). It's public.
4. Create src/routes/api/ticketping-token/+server.ts exactly as on the docs page, adapting only the user lookup:
401 when signed out, otherwise a text/plain JWT from jose,
new SignJWT({ email, name }).setProtectedHeader({ alg: 'HS256' }).setSubject(String(user.id)).setExpirationTime('5m'),
signed with the TICKETPING_IDENTITY_SECRET from $env/dynamic/private.
If this app's API is a separate backend (another origin, token auth), skip this route: sign the token in that
backend instead and point getToken at it, as in "If your API runs on another origin" on the docs page.
5. Add TICKETPING_IDENTITY_SECRET= to .env (not committed). Never import it from $env/static/public or $env/dynamic/public.
6. Run the production build and load a few pages with the preview server; they must return 200.
7. Tell me how to verify: sign in on localhost, fetch a token from the console, and paste it into the
token validator at Settings → API → Identity secret.