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
- Your page calls
identifywith agetTokenfunction. - The widget calls
getToken, which asks a route on your server for a token. - Your route checks who’s signed in and signs a token with your identity secret. That’s a JWT with a 5-minute expiry.
- 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:
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 answers401when nobody is signed in. - Signs
{ sub, email, name, exp }with HS256, whereexpis 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.
| Claim | Required | Rules |
|---|---|---|
sub | Yes | Your user ID, as a string of 1 to 255 characters. Convert numeric IDs to strings |
exp | Yes | Expiry in seconds since the epoch (not milliseconds), at most 10 minutes from now. Use 5 minutes |
email | No | A valid email, at most 254 characters |
name | No | At most 255 characters |
company | No | { "id": "acme", "name": "Acme" }. id is 1 to 255 characters; name is optional |
attributes | No | A 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, nbf | No | Checked when present. Many libraries add iat for you |
aud | No | If present, must be "ticketping" or a list that contains it |
- Only
HS256is 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:
| Reason | What it means | How to fix it |
|---|---|---|
token_too_large | The token is over 4096 bytes | Send fewer or shorter attributes |
token_malformed | It isn’t a JWT | Return the token itself as plain text, not JSON like {"token": "..."}, and without quotes |
alg_not_allowed | It’s signed with something other than HS256 | Pass HS256 as the algorithm |
signature_invalid | The signature doesn’t match your identity secret | Check TICKETPING_IDENTITY_SECRET on the server: whitespace, an old value after rotation, or a different team’s secret |
claim_missing | sub or exp is missing (claim says which) | Add it |
claim_invalid | A 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_invalid | aud is set to something other than "ticketping" | Set aud to "ticketping", or leave it out |
token_not_yet_valid | iat or nbf is more than 60 seconds in the future | Fix your server’s clock (enable NTP) |
token_expired | exp passed more than 60 seconds ago | Sign a new token for each request, and check your server’s clock |
lifetime_too_long | exp is more than 10 minutes away | Use 5 minutes |
token_replayed | This token was already used | Sign 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
- Run your app on localhost and sign in.
- In the browser console, run
await (await fetch('/api/ticketping-token', { method: 'POST' })).text(). Add your CSRF header if your framework needs one. - 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.
- 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
- Choose Rotate Secret and copy the new secret.
- The old secret keeps working for 24 hours. Deploy the new value as
TICKETPING_IDENTITY_SECRETbefore then. - 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:
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:
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
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.