Authentication and identity
The two ways a chat client authenticateslink
Every chat client - the web widget, Android, iOS, React Native and Flutter - authenticates the same way, and identifies the customer in one of two ways.
| Mode A: publishable widget key | Mode B: backend-minted identity token | |
|---|---|---|
| What you ship in client code | The key ID only (sk_live_abc123) | The same key ID, plus a short-lived token |
| Who the customer is | Whoever they type into the pre-chat form, or nothing at all | Whoever your backend said they are |
| Can the client claim to be someone else | Yes - the name and email are just strings | No - identity comes from a signed token |
| Needs a backend call | No | Yes, one call per customer session |
Both modes use the same publishable key. Mode B adds a token; it does not replace the key.
The publishable widget key (required in both modes)link
Copy the key ID from Dashboard -> Org -> Chat. It looks like sk_live_abc123, and it is deliberately secret-less: server-side the session is marked widget-only and clamped to the chat operation whitelist, so a key ID in page source, in an APK, or in an IPA exposes nothing beyond chat.
Anonymous chat (Mode A)link
Supply the key and nothing else. The visitor gets the pre-chat form, and the conversation is anonymous.
<script src="https://widget.newinstance.cloud/embed.js"
data-api-key="sk_live_abc123"
></script>You may pre-fill customerName / customerEmail to skip the form. Doing so is a convenience, not authentication: those values arrive from the browser and the backend treats them as untrusted display data.
Anonymous chat is fully supported and is not going away. Use Mode B when you need the conversation tied to a real account.
Authenticated chat (Mode B)link
Your backend --(id, name, email, metadata + secret key)--> createCsCustomerToken
Your backend <--(signed token, expires)-------------------
Your frontend --(token only)------------------------------> Chat SDK
Chat SDK --(token)-----------------------------------> Chat backend
verifies signature
derives the customer
attaches the conversationThe client never sees your secret key, and never has to repeat the customer's details: the token carries them.
The natural place to mint one is wherever your app establishes a session. When a user signs in, mint a chat token alongside your own session token and return it in the same response; your frontend then hands that one string to the chat SDK. Nothing about the customer travels through the browser as an assertion the backend has to take on trust.
Everything in the payload - name, email, phone, and whatever you put in metadata - is sealed into the token and signed. The chat backend verifies the signature before reading any of it, so a tampered page cannot change a plan tier or swap an account number. Verified metadata is stored on the conversation and shown to your agents.
Because tokens are minted per sign-in, a returning customer arrives with a fresher one. When they resume a conversation, the verified name, email and metadata on it are refreshed from the new token, so an agent picking up a week-old thread sees the customer's current plan rather than whatever was true when the conversation opened.
Token APIlink
Two doors onto the same session store. Both read and write it directly, so they enforce identical rules and neither is a wrapper around the other. Use whichever suits your backend.
| REST (recommended) | GraphQL | |
|---|---|---|
| Create | POST {{baseUrl}}/api/v1/chat/sessions | createCsCustomerToken |
| Check if revoked | GET {{baseUrl}}/api/v1/chat/sessions/{sessionId} | csCustomerSessionRevoked |
| Find live | GET {{baseUrl}}/api/v1/chat/sessions?customerId= | csCustomerSessionForCustomer |
| Invalidate | DELETE {{baseUrl}}/api/v1/chat/sessions/{sessionId}?customerId= | revokeCsCustomerSession |
| Invalidate by customer | DELETE {{baseUrl}}/api/v1/chat/sessions?customerId= | revokeCsCustomerSessionForCustomer |
The REST endpoints are documented with runnable examples under Session tokens (REST API). The GraphQL endpoint is POST {{serviceUrl}}/service.
A publishable widget key is rejected: on REST because the key format is refused outright, and on GraphQL by the widget operation whitelist and again inside the resolver. Minting requires the secret, which is what stops a browser from minting an identity for somebody else.
Session lifetime and the one-session rulelink
- You choose the lifetime.
expiresInSecondsaccepts 60 to 86400 seconds. Six-hour sessions? Send21600. Anything above 24 hours is refused rather than silently clamped. - One live session per customer. While a customer's session is still valid, creating another returns 409 (GraphQL:
CONFLICT) carrying the session that is in the way. Mint again once it expires, or revoke it explicitly first. - Revocation is immediate. An invalidated token stops being accepted on the next request, even though its signature is still valid and it has not expired.
- Expiry needs no cleanup. The token carries its own
exp, and the two Redis keys carry the same TTL, so everything disappears on its own. - Nothing about the customer is stored. The JWT carries the identity; the platform keeps no copy that could drift from the signed claims. Decode the token if you need it back.
- The token is never stored either. Lose it and you revoke the session and mint another.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | String | Yes | Your stable identifier for this customer. Becomes the trusted customer id on the conversation. Max 200 characters. |
customerName | String | Yes | Display name attributed to the customer's messages. Max 200 characters. |
customerEmail | String | No | Max 320 characters. Also satisfies the merchant's Require customer email setting, so the visitor is never asked for it. |
customerPhone | String | No | Max 40 characters. Stored on the conversation and shown to agents. |
metadata | JSONObject | No | Any extra context your agents should see: plan tier, account number, locale, lifetime value. Flat key/value pairs; values are coerced to strings. Sealed into the token, so it arrives verified and the client never sends it. |
expiresInSeconds | Int | No | Requested lifetime. Clamped to 60 - 86400. Defaults to 3600 (one hour). |
Metadata limits
At most 20 keys, keys up to 64 characters, values up to 500 characters, 4096 bytes serialized in total. Values may be strings, numbers or booleans; nested objects and arrays are rejected, not flattened, so you always get what you asked for. Anything over a limit fails the mint with BAD_USER_INPUT rather than being silently truncated - you find out at integration time, not from a half-empty agent console.
The caps exist because the whole claim set is signed into a token that travels in a URL fragment on the web.
REST
curl -X POST "{{baseUrl}}/api/v1/chat/sessions" \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_abc123:YOUR_SECRET_KEY" \
-d '{
"customerId": "usr_123",
"customerName": "Ada Lovelace",
"customerEmail": "ada@example.com",
"customerPhone": "+44 20 7946 0958",
"metadata": { "plan": "enterprise", "accountNumber": "AC-4471" },
"expiresInSeconds": 21600
}'GraphQL
curl -X POST "{{serviceUrl}}/service" \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_abc123:YOUR_SECRET_KEY" \
-d '{
"query": "mutation($input: CsCustomerTokenInput!) { createCsCustomerToken(input: $input) { token expiresAt expiresInSeconds } }",
"variables": {
"input": {
"customerId": "usr_123",
"customerName": "Ada Lovelace",
"customerEmail": "ada@example.com",
"customerPhone": "+44 20 7946 0958",
"metadata": {
"plan": "enterprise",
"accountNumber": "AC-4471",
"lifetimeValueUsd": 18400,
"supportTier": "gold"
},
"expiresInSeconds": 3600
}
}
}'Response
{
"data": {
"createCsCustomerToken": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfMTIzIiwi...",
"expiresAt": "2026-08-26T15:04:05.000Z",
"expiresInSeconds": 3600
}
}
}| Field | Meaning |
|---|---|
token | Hand this to the chat SDK. Treat it like a session credential. |
expiresAt | Absolute expiry. After this the SDK needs a fresh token. |
expiresInSeconds | The lifetime actually granted, after clamping. |
Errors
| Condition | Result |
|---|---|
| Publishable key used instead of a secret key | UNAUTHENTICATED - "Chat identity tokens must be minted server side with a secret API key" |
customerId or customerName missing or blank | BAD_USER_INPUT |
| A field exceeds its length cap | BAD_USER_INPUT |
customerPhone longer than 40 characters | BAD_USER_INPUT |
metadata is not an object, nests objects or arrays, or exceeds a limit | BAD_USER_INPUT, naming the offending key |
| Key revoked, expired, or from the wrong environment | UNAUTHENTICATED |
Error responses never echo the key, the secret, or the token.
Token propertieslink
- Signed with HMAC-SHA256 over a compact JWS, using a per-organization secret that is generated on first use, encrypted at rest, and never leaves the server. It is not returned by any API.
- Scoped to one organization. A token minted for org A is rejected by org B even though the format is identical.
- Expiring. Past
expiresAtit is refused. - Not revocable individually. To invalidate every outstanding token for an organization, rotate its secret; existing tokens stop verifying immediately.
- Claims are never trusted without verification. The signature is checked on every use, and the
algheader is ignored (HS256 with the organization's secret or nothing), so analg: nonetoken is refused.
The token replaces the initialization fieldslink
This is the point of the payload above. Chat initialization needs a name, usually an email, sometimes a phone number and whatever else your agents need to see. Without a token, each of those has to be handed to the SDK on the client, where the backend has to take them on trust.
With a token, you supply them once, from your server, at mint time. They are signed into the token, so the chat session already knows who this person is before the client says anything:
| Supplied at mint time | Where it lands |
|---|---|
customerName | The name on the conversation and on every message the customer sends |
customerEmail | The conversation's email, and it satisfies Require customer email so the pre-chat form is skipped |
customerPhone | Shown on the conversation in the agent console |
customerId | The verified customer id, used to reunite this person with their earlier conversations |
metadata | Shown to the agent as extra context |
The client then passes one string. It never repeats the name, never repeats the email, and never gets asked to fill in a pre-chat form.
Because the payload is signed, none of it can be edited in the browser or in a repackaged app: change one character and the signature stops matching and the token is refused.
Using the token in each SDKlink
The token is the identity. Do not pass the customer's name, email, phone or id alongside it - the server ignores them and derives everything from the signed claims.
<!-- Web: script tag -->
<script src="https://widget.newinstance.cloud/embed.js"
data-api-key="sk_live_abc123"
data-customer-token="TOKEN_FROM_YOUR_BACKEND"
></script>NInstanceChat.init({
apiKey: 'sk_live_abc123',
customerToken: tokenFromYourBackend,
});val chat = LiveAndAiChat.Builder(context)
.config(LiveAndAiChatConfig(apiKey = "sk_live_abc123"))
.user(ChatUser.fromToken(tokenFromYourBackend))
.build()let chat = try LiveAndAiChat.Builder()
.config(try LiveAndAiChatConfig(apiKey: "sk_live_abc123"))
.user(ChatUser.fromToken(tokenFromYourBackend))
.build()const chat = new NewinstanceChat({ apiKey: 'sk_live_abc123' });
await chat.ready;
chat.setUserToken(tokenFromYourBackend);
await chat.initialize();final chat = NewinstanceChat(NewinstanceChatConfig(apiKey: 'sk_live_abc123'));
await chat.ready;
await chat.setUserToken(tokenFromYourBackend);
await chat.initialize();The web widget carries the token in the iframe URL fragment, which browsers do not send to the server, so it cannot appear in the widget host's access logs.
Token lifecyclelink
Mint a session when your app knows who the user is - at sign-in - and return the token with your own session so your frontend can hand it straight to the chat SDK.
Because a customer may hold only one live session, do not blindly mint on every page load: return the token you already issued for that sign-in. If you did not keep it, either wait for expiry or DELETE the session and mint a fresh one. GET /api/v1/chat/sessions?customerId= tells you what the customer currently holds.
On sign-out, call DELETE /api/v1/chat/sessions?customerId=.... It is idempotent, so it is safe to wire into your own sign-out unconditionally.
A token that has expired or been revoked surfaces as the error code INVALID_IDENTITY_TOKEN (see Diagnosing problems below). Handle it by minting a new session; do not fall back to anonymous, because the SDK will not silently downgrade an identified session.
What customerId does without a tokenlink
The mobile SDKs accept customerId on ChatUser without a token. It is recorded on the conversation for agent context, and it is not treated as identity: it never matches the customer against other conversations. Trusting a client-asserted id would let anyone resume someone else's chat by guessing it. Use a token when you need the id to mean something.
Diagnosing problemslink
Every SDK raises diagnostics on an error channel that is separate from the chat interface. The customer never sees these; you do.
| Code | What went wrong | What to do |
|---|---|---|
MISSING_WIDGET_KEY | No API key was supplied | Pass the publishable key ID |
INVALID_PUBLIC_KEY | The key was rejected | Check it exists, is active, and matches the environment (sk_test_ vs sk_live_) |
CHAT_CONFIG_UNAVAILABLE | The key is fine, but the organization has no chat configuration | Configure chat in the dashboard |
CHAT_DISABLED | Chat is switched off, or both the AI and live-agent modules are off | Enable it in the dashboard |
CONFIG_FETCH_FAILED | Remote configuration could not be loaded | Usually transient. The chat keeps working on its local or built-in theme |
INVALID_IDENTITY_TOKEN | The token was malformed, expired, or signed for another organization | Mint a fresh token |
NETWORK_ERROR | The backend could not be reached | Check connectivity and any content policy |
TRANSPORT_ERROR | The realtime connection dropped | Usually transient; the SDK reconnects |
CHAT_INITIALIZATION_FAILED | The conversation could not be started | Retry; check the other codes first |
MESSAGE_SEND_FAILED | A message could not be delivered | Retry the send |
ATTACHMENT_FAILED | An attachment could not be uploaded | Check size and type limits |
SERVER_ERROR | The backend answered with something unusable | Retry; contact support with the code |
WIDGET_UNREACHABLE | Web only: the widget iframe never answered | Check the widget URL is reachable and not blocked |
Diagnostics carry a code, a developer-facing message, and recoverable. They never contain the API key, the identity token, or customer personal data.
Security checklistlink
- The secret key stays on your server. Only the key ID ships in client code.
- Mint tokens from your backend, after your authentication has established who the user is.
- Never mint a token from a request whose customer id came straight from the browser.
- Keep token lifetimes short; the default hour is a reasonable starting point.
- Rotate an organization's signing secret to invalidate every outstanding token for it.