NewInstance Documentation
View as Markdown

Authentication and identity

From 0.4.0, every chat client (the web widget, Android, iOS, React Native and Flutter) authenticates in exactly one of two modes. They are mutually exclusive: configure one credential, never both.

Customer token mode (recommended)Key mode (trusted internal apps only)
What the client is givencustomerToken: a short-lived token your backend mintsapiKey: the full keyId:secret
What the SDK sends on every requestAuthorization: Bearer <token>x-api-key: keyId:secret
Who the customer isWhoever your backend said they are, from signed claimsWhatever the app passes to setUser
Is the secret in client codeNo, it stays on your serverYes, it ships inside the app
Needs a backend callYes, one per customer sessionNo
Use it forEvery app and website distributed to the publicInternal tools, staff apps and devices you control

Configuring both credentials, or neither, fails with E_CONFIG and the message pass either apiKey (keyId:secret) or customerToken. An apiKey without its secret fails with apiKey must be keyId:secret, or use customerToken. The web widget is the one exception to the second rule; see The deprecated bare key ID below.

Your backend  --(id, name, email, metadata + x-api-key: keyId:secret)-->  POST /api/v1/chat/sessions
Your backend  <--(signed token, expiresAt)-------------------------------
Your frontend --(token only)-------------------------------------------->  Chat SDK
Chat SDK      --(Authorization: Bearer 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. No API key ships in the client at all.

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.

Key mode (trusted internal apps only)link

Key mode ships your secret inside the client app. Use it only for trusted internal apps, never for apps or websites distributed to the public. Anyone who can read the page source or unpack the app can copy the full key.

The client is configured with the full keyId:secret, which the SDK sends whole in x-api-key, and names the customer with setUser({ customerName, customerEmail?, customerId? }). Those customer fields come from the client, so use customer token mode whenever the identity must be verified. setUserToken is not available in key mode and fails with E_CONFIG.

The deprecated bare key IDlink

Before 0.4.0, chat clients sent only the key ID (sk_live_abc123) as a publishable widget key, optionally with a customer token passed inside the chat initialization. That form is deprecated, and nothing deployed breaks:

  • The server still accepts a bare key ID, alone or with a customer token, from deployed web embeds and SDKs below 0.4.0.
  • On the web, data-api-key="sk_live_abc123" (or apiKey without a :) keeps the old behaviour, including anonymous visitors and the pre-chat form, and logs one console warning per page load.
  • The 0.4.0 Android, iOS, React Native and Flutter SDKs reject a bare key ID with E_CONFIG.

Do not use it for new integrations. To migrate a public site or app, mint a customer token on your server and pass only the token, dropping the key ID from the client. If a page has no signed-in customer, your backend can mint a token for a guest id it generates. Keep key mode for internal apps.

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 bare key ID 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.
  • Rotating before expiry. To hand a running client a fresh token for the same customer, revoke the current session (DELETE /api/v1/chat/sessions?customerId=), mint a new one, and pass it to the SDK's setUserToken. Minting while the old session is still valid returns 409.
  • 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.

Requestlink

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 limitslink

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, so 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.

RESTlink

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
  }'

GraphQLlink

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
      }
    }
  }'

Responselink

{
  "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.

Errorslink

ConditionResult
A bare key ID used instead of the full keyId:secretUNAUTHENTICATED: "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.

Customer token mode in each SDKlink

The token is the identity. Pass no API key and none of the customer's name, email, phone or id alongside it: the SDK sends only Authorization: Bearer <token>, and the server derives everything from the signed claims. When a fresh token is due, hand it to the running SDK with setUserToken; it swaps the credential and reconnects without tearing the chat down, and keeps the conversation when the new token is for the same customer.

<!-- Web: script tag -->
<script src="https://widget.newinstance.cloud/embed.js"
  data-customer-token="TOKEN_FROM_YOUR_BACKEND"
></script>
const widget = NInstanceChat.init({ customerToken: tokenFromYourBackend });
widget.setUserToken(freshTokenFromYourBackend);
val chat = LiveAndAiChat.Builder(context)
    .config(LiveAndAiChatConfig(customerToken = tokenFromYourBackend))
    .build()
chat.initialize()

chat.setUserToken(freshTokenFromYourBackend)
let chat = try LiveAndAiChat.Builder()
    .config(try LiveAndAiChatConfig(customerToken: tokenFromYourBackend))
    .build()
chat.initialize()

try chat.setUserToken(freshTokenFromYourBackend)
const chat = new NewinstanceChat({ customerToken: tokenFromYourBackend });
await chat.ready;
await chat.initialize();

chat.setUserToken(freshTokenFromYourBackend);
final chat = NewinstanceChat(NewinstanceChatConfig(customerToken: tokenFromYourBackend));
await chat.ready;
await chat.initialize();

await chat.setUserToken(freshTokenFromYourBackend);

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. setUserToken on the web posts the new token to the widget iframe, targeted at the widget origin, without reloading it.

Key mode in each SDKlink

Trusted internal apps only. Pass the full key and name the customer with setUser; do not pass customerToken.

const internal = NInstanceChat.init({ apiKey: 'sk_live_abc123:YOUR_SECRET_KEY' });
internal.setUser({ customerName: 'Ada Lovelace', customerEmail: 'ada@example.com', customerId: 'usr_123' });
val chat = LiveAndAiChat.Builder(context)
    .config(LiveAndAiChatConfig(apiKey = "sk_live_abc123:YOUR_SECRET_KEY"))
    .build()
chat.setUser(ChatUser(customerName = "Ada Lovelace", customerEmail = "ada@example.com", customerId = "usr_123"))
chat.initialize()
let chat = try LiveAndAiChat.Builder()
    .config(try LiveAndAiChatConfig(apiKey: "sk_live_abc123:YOUR_SECRET_KEY"))
    .build()
chat.setUser(ChatUser(customerName: "Ada Lovelace", customerId: "usr_123", customerEmail: "ada@example.com"))
chat.initialize()
const chat = new NewinstanceChat({ apiKey: 'sk_live_abc123:YOUR_SECRET_KEY' });
await chat.ready;
chat.setUser({ customerName: 'Ada Lovelace', customerEmail: 'ada@example.com', customerId: 'usr_123' });
await chat.initialize();
final chat = NewinstanceChat(NewinstanceChatConfig(apiKey: 'sk_live_abc123:YOUR_SECRET_KEY'));
await chat.ready;
await chat.setUser(const ChatUser(customerName: 'Ada Lovelace', customerEmail: 'ada@example.com', customerId: 'usr_123'));
await chat.initialize();

On the web the full key reaches the widget iframe through the URL fragment, never the query string. It is still readable by anyone who can open the page, which is why key mode is for internal pages only.

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.

To keep a long session going, rotate before expiresAt: revoke the current session, mint a new one, and pass it to setUserToken. Choosing a lifetime that covers a typical customer session means most customers never need a rotation.

A token that has expired or been revoked surfaces as the error code INVALID_IDENTITY_TOKEN (see Diagnosing problems below). The SDK stops and does not reconnect with the same token. Handle it by minting a new session and calling setUserToken, which restarts the chat; the SDK will not silently downgrade an identified session.

Customer fields in key modelink

In key mode the customer comes from setUser({ customerName, customerEmail?, customerId? }). Those values are asserted by the client app, which is acceptable only because key mode is limited to apps you control. When the customer's identity must be verified, use customer token mode. In customer token mode setUser is still accepted, but its fields are not sent anywhere: the token is the identity.

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
E_CONFIGThe SDK was configured with both credentials, neither, or a key without its secretPass exactly one: customerToken, or apiKey as keyId:secret
MISSING_WIDGET_KEYNo credential was suppliedPass a customer token, or the full key in key mode
INVALID_PUBLIC_KEYKey mode: the key was rejectedCheck it exists, is active, is passed as the full keyId:secret, 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_TOKENCustomer token mode: the token was malformed, expired, revoked, or signed for another organizationMint a fresh token and pass it to setUserToken
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

  • For anything public, the secret key stays on your server and the client gets only a customer token. Key mode, which ships the secret, is for trusted internal apps only.
  • 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.