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 keyMode B: backend-minted identity token
What you ship in client codeThe key ID only (sk_live_abc123)The same key ID, plus a short-lived token
Who the customer isWhoever they type into the pre-chat form, or nothing at allWhoever your backend said they are
Can the client claim to be someone elseYes - the name and email are just stringsNo - identity comes from a signed token
Needs a backend callNoYes, 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 conversation

The 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
CreatePOST {{baseUrl}}/api/v1/chat/sessionscreateCsCustomerToken
Check if revokedGET {{baseUrl}}/api/v1/chat/sessions/{sessionId}csCustomerSessionRevoked
Find liveGET {{baseUrl}}/api/v1/chat/sessions?customerId=csCustomerSessionForCustomer
InvalidateDELETE {{baseUrl}}/api/v1/chat/sessions/{sessionId}?customerId=revokeCsCustomerSession
Invalidate by customerDELETE {{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. expiresInSeconds accepts 60 to 86400 seconds. Six-hour sessions? Send 21600. 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

FieldTypeRequiredNotes
customerIdStringYesYour stable identifier for this customer. Becomes the trusted customer id on the conversation. Max 200 characters.
customerNameStringYesDisplay name attributed to the customer's messages. Max 200 characters.
customerEmailStringNoMax 320 characters. Also satisfies the merchant's Require customer email setting, so the visitor is never asked for it.
customerPhoneStringNoMax 40 characters. Stored on the conversation and shown to agents.
metadataJSONObjectNoAny 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.
expiresInSecondsIntNoRequested 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
    }
  }
}
FieldMeaning
tokenHand this to the chat SDK. Treat it like a session credential.
expiresAtAbsolute expiry. After this the SDK needs a fresh token.
expiresInSecondsThe lifetime actually granted, after clamping.

Errors

ConditionResult
Publishable key used instead of a secret keyUNAUTHENTICATED - "Chat identity tokens must be minted server side with a secret API key"
customerId or customerName missing or blankBAD_USER_INPUT
A field exceeds its length capBAD_USER_INPUT
customerPhone longer than 40 charactersBAD_USER_INPUT
metadata is not an object, nests objects or arrays, or exceeds a limitBAD_USER_INPUT, naming the offending key
Key revoked, expired, or from the wrong environmentUNAUTHENTICATED

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 expiresAt it 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 alg header is ignored (HS256 with the organization's secret or nothing), so an alg: none token 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 timeWhere it lands
customerNameThe name on the conversation and on every message the customer sends
customerEmailThe conversation's email, and it satisfies Require customer email so the pre-chat form is skipped
customerPhoneShown on the conversation in the agent console
customerIdThe verified customer id, used to reunite this person with their earlier conversations
metadataShown 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.

CodeWhat went wrongWhat to do
MISSING_WIDGET_KEYNo API key was suppliedPass the publishable key ID
INVALID_PUBLIC_KEYThe key was rejectedCheck it exists, is active, and matches the environment (sk_test_ vs sk_live_)
CHAT_CONFIG_UNAVAILABLEThe key is fine, but the organization has no chat configurationConfigure chat in the dashboard
CHAT_DISABLEDChat is switched off, or both the AI and live-agent modules are offEnable it in the dashboard
CONFIG_FETCH_FAILEDRemote configuration could not be loadedUsually transient. The chat keeps working on its local or built-in theme
INVALID_IDENTITY_TOKENThe token was malformed, expired, or signed for another organizationMint a fresh token
NETWORK_ERRORThe backend could not be reachedCheck connectivity and any content policy
TRANSPORT_ERRORThe realtime connection droppedUsually transient; the SDK reconnects
CHAT_INITIALIZATION_FAILEDThe conversation could not be startedRetry; check the other codes first
MESSAGE_SEND_FAILEDA message could not be deliveredRetry the send
ATTACHMENT_FAILEDAn attachment could not be uploadedCheck size and type limits
SERVER_ERRORThe backend answered with something unusableRetry; contact support with the code
WIDGET_UNREACHABLEWeb only: the widget iframe never answeredCheck 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.