Identity verification

Sign a short-lived token on your server so the widget knows who your signed-in users are, and nobody can pretend to be them.

When you identify a signed-in user with a token, they see their conversations on every device, and your team sees a Verified badge with their name and email. The token proves the details came from your server, not from someone typing into the browser console.

How it works

  1. Your page calls identify with a getToken function.
  2. The widget calls getToken, which asks a route on your server for a token.
  3. Your route checks who’s signed in and signs a token with your identity secret. That’s a JWT with a 5-minute expiry.
  4. The widget sends the token to Ticketping, which checks the signature and links the visitor to that user.

The widget calls getToken again whenever it needs a fresh token, such as after a long time away. There’s no Ticketping SDK on the server: any JWT library works, and the route is about 15 lines.

Get your identity secret

In Settings → API → Identity secret, choose Create Secret and copy it. It’s shown once. Owners and admins can create and rotate it.

Put it in your server’s environment as TICKETPING_IDENTITY_SECRET:

bash
TICKETPING_IDENTITY_SECRET=tpis_...

There’s one secret per team, and it’s the same in local development, staging and production. Keep it on the server. Never put it in frontend code, a public environment variable, or your repository.

Add the token route for your stack

TanStack Start, React Router, Nuxt and Astro routes are on their install pages.

Every route does the same four things:

  • Accepts POST, and answers 401 when nobody is signed in.
  • Signs { sub, email, name, exp } with HS256, where exp is 5 minutes from now.
  • Returns the token as plain text, not JSON.
  • Signs a new token on every request. Don’t cache tokens.

Token format

The smallest valid token is { "sub": "<user id>", "exp": <now + 300> }, signed with HS256. Add email and name so your team knows who they’re talking to.

ClaimRequiredRules
subYesYour user ID, as a string of 1 to 255 characters. Convert numeric IDs to strings
expYesExpiry in seconds since the epoch (not milliseconds), at most 10 minutes from now. Use 5 minutes
emailNoA valid email, at most 254 characters
nameNoAt most 255 characters
companyNo{ "id": "acme", "name": "Acme" }. id is 1 to 255 characters; name is optional
attributesNoA flat object of up to 50 keys. Keys are letters, digits and underscores (not starting with a digit), up to 64 characters. Values are strings up to 1024 characters, numbers, booleans or null
iat, nbfNoChecked when present. Many libraries add iat for you
audNoIf present, must be "ticketping" or a list that contains it
  • Only HS256 is accepted.
  • The whole token is at most 4096 bytes.
  • Leave out claims you don’t have. Don’t send "email": null.
  • Other claims and headers (jti, kid, iss, role, …) are ignored. Ticketping never trusts them.
  • The HMAC key is the secret string exactly as shown, as UTF-8 bytes. Don’t base64-decode it.

Claims in the token always win over details passed to identify in the browser. Put anything your team relies on, like email or company, in the token.

Each token works once

Ticketping remembers every token it accepts until it expires, and refuses it a second time with token_replayed. You don’t need to do anything for this: just sign a new token for each request, which the routes on these pages already do.

The token validator in the dashboard never marks a token as used, so you can test a token there and still use it.

When a token is refused

The identify call fails with 400 token_invalid, and the widget stays anonymous. The response has a reason and a message saying what to fix. Checks run in this order, and the first failure is the one you see:

ReasonWhat it meansHow to fix it
token_too_largeThe token is over 4096 bytesSend fewer or shorter attributes
token_malformedIt isn’t a JWTReturn the token itself as plain text, not JSON like {"token": "..."}, and without quotes
alg_not_allowedIt’s signed with something other than HS256Pass HS256 as the algorithm
signature_invalidThe signature doesn’t match your identity secretCheck TICKETPING_IDENTITY_SECRET on the server: whitespace, an old value after rotation, or a different team’s secret
claim_missingsub or exp is missing (claim says which)Add it
claim_invalidA claim has the wrong type or format (claim says which)Common causes: a number for sub, exp in milliseconds, null for an optional claim, a nested object in attributes
audience_invalidaud is set to something other than "ticketping"Set aud to "ticketping", or leave it out
token_not_yet_validiat or nbf is more than 60 seconds in the futureFix your server’s clock (enable NTP)
token_expiredexp passed more than 60 seconds agoSign a new token for each request, and check your server’s clock
lifetime_too_longexp is more than 10 minutes awayUse 5 minutes
token_replayedThis token was already usedSign a new token for each request. Don’t cache tokens on the server or in the browser

Ticketping allows 60 seconds of clock difference for the time checks.

Test your route

  1. Run your app on localhost and sign in.
  2. In the browser console, run await (await fetch('/api/ticketping-token', { method: 'POST' })).text(). Add your CSRF header if your framework needs one.
  3. Paste the token into the token validator under Settings → API → Identity secret. It shows the claims Ticketping would accept, or the reason and how to fix it.
  4. Reload your app and send a message. In the dashboard it shows the user’s name and a Verified badge.

The examples on these pages are tested against the shared token test vectors. You can run the same vectors in your own test suite.

Rotate the secret

  1. Choose Rotate Secret and copy the new secret.
  2. The old secret keeps working for 24 hours. Deploy the new value as TICKETPING_IDENTITY_SECRET before then.
  3. Once every server uses the new value, choose Revoke Now to stop the old one right away, or let it expire.

There are never more than two working secrets, so there’s no key ID to manage. If a secret leaks, rotate and then revoke the old one immediately. Revoking takes effect at once.

Sign-out and switching users

Call logout when the user signs out:

js
Ticketping('logout')

The widget forgets the user and starts again as a new anonymous visitor, so the next person on the same computer can’t see their conversations. The adapters do this for you when user becomes null.

If a different user signs in without a sign-out in between, call identify with the new userId. The widget logs the first user out before identifying the second. Sign-out in one tab applies to every open tab.

Unverified identity

identify without getToken sends the details unsigned:

js
Ticketping('identify', { email: 'ada@acme.com', name: 'Ada Lovelace' })

Your team sees them with an Unverified badge. The visitor gets no history from other devices, and nothing is matched by email, because anyone could type any email.

In the widget’s Security tab:

  • Require verified identity refuses identify calls without a token. Turn it on once your token route works.
  • Identified users only shows anonymous visitors a “Log in to chat” button that links to your login URL, instead of the message box.

Copy prompt for your AI coding agent

prompt
Add Ticketping identity verification (chat widget v2) to this project.

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

1. Work out the backend stack and open the matching page from the docs index (identity-django, identity-fastapi,
   identity-flask, identity-node, identity-rails, identity-go, identity-laravel, or the framework install page).
   Copy its token route exactly, adapting only how the signed-in user is looked up.
2. The route: POST /api/ticketping-token, 401 when signed out, otherwise a plain-text HS256 JWT with
   sub (the user ID as a string), email and name when present (omit, never null), and exp = now + 300 seconds,
   signed with the TICKETPING_IDENTITY_SECRET env var. Sign a new token on every request.
3. Add TICKETPING_IDENTITY_SECRET= to the server env file and its example (not committed). Never expose it to the browser.
4. In the frontend, call Ticketping('identify', { userId, email, name, getToken }) for the signed-in user, where
   getToken POSTs to the route (with the CSRF header this framework needs) and returns res.text(); throw if !res.ok.
   Call Ticketping('logout') on sign-out.
5. 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.