Anyone can call identify in a browser with any user ID, so Gleap needs proof that the user really is who your app says. Your backend gives that proof: after login it signs a short identity token (a JWT) for the user, and your app hands it to Gleap. Gleap checks the token once and answers with a session grant that the SDK sends with every later request. Everything inside the token is signed: the user ID, the email, the company and attributes such as stripeCustomerId.
How it works
Create an identity key in Settings → Security → Customer identity.
After your user logs in, your backend signs a token with that key.
Your app passes the token to Gleap with
setUserJwt, after login and on every app start.Gleap stores the signed fields on the contact and returns a session grant. The SDK sends it with every request, so Gleap knows each request is signed without checking the token again.
Create an identity key
Open Settings → Security → Customer identity → Identity keys and click Create key.
Give it a label, for example "Production backend", and pick the platform it may sign for: all platforms (the default), web, iOS or Android.
Copy the key ID and the secret. Gleap shows the secret once.
Store both in your backend's configuration, for example as
GLEAP_IDENTITY_KEY_IDandGLEAP_IDENTITY_SECRET.
A project holds up to five keys. Only admins can see and manage identity keys.
Sign the token on your backend
The token is an HS256 JWT with the key ID in the kid header. Everything the contact should show goes into the token.
Claim | |
|---|---|
| Required. Your stable user ID as a string (at most 200 characters). |
| Set it. Expiry in seconds since the epoch, 5 to 60 minutes ahead. It only gates the sign-in; the session itself runs on the grant. A token without |
| Optional. Only an email your system has confirmed. Signing it means you vouch for it. |
| Optional. The user's name. |
| Optional. The user's phone number. |
| Optional. An object with |
anything else | A signed attribute: string, number or boolean, stored as custom data. Use |
Limits: at most 50 attributes, names of at most 64 characters (letters, digits and _), string values of at most 1,000 characters, and at most 4,096 characters for the whole token. Gleap allows 60 seconds of clock skew.
// npm install jsonwebtoken
const jwt = require("jsonwebtoken");
const token = jwt.sign(
{
userId: String(user.id),
email: user.email,
name: user.name,
company: { id: user.companyId, name: user.companyName },
stripeCustomerId: user.stripeCustomerId,
},
process.env.GLEAP_IDENTITY_SECRET,
{ algorithm: "HS256", keyid: process.env.GLEAP_IDENTITY_KEY_ID, expiresIn: "30m" }
);// composer require firebase/php-jwt
use Firebase\JWT\JWT;
$token = JWT::encode(
[
'userId' => (string) $user->id,
'email' => $user->email,
'name' => $user->name,
'company' => ['id' => $user->companyId, 'name' => $user->companyName],
'exp' => time() + 1800,
],
getenv('GLEAP_IDENTITY_SECRET'),
'HS256',
getenv('GLEAP_IDENTITY_KEY_ID')
);# pip install PyJWT
import os
from datetime import datetime, timedelta, timezone
import jwt
token = jwt.encode(
{
"userId": str(user.id),
"email": user.email,
"name": user.name,
"company": {"id": user.company_id, "name": user.company_name},
"exp": datetime.now(timezone.utc) + timedelta(minutes=30),
},
os.environ["GLEAP_IDENTITY_SECRET"],
algorithm="HS256",
headers={"kid": os.environ["GLEAP_IDENTITY_KEY_ID"]},
)# gem 'jwt'
require 'jwt'
token = JWT.encode(
{
userId: current_user.id.to_s,
email: current_user.email,
name: current_user.name,
company: { id: current_user.company_id, name: current_user.company_name },
exp: Time.now.to_i + 1800
},
ENV.fetch('GLEAP_IDENTITY_SECRET'),
'HS256',
{ kid: ENV.fetch('GLEAP_IDENTITY_KEY_ID') }
)// dotnet add package System.IdentityModel.Tokens.Jwt
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("GLEAP_IDENTITY_SECRET")))
{
KeyId = Environment.GetEnvironmentVariable("GLEAP_IDENTITY_KEY_ID"),
};
var header = new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
var payload = new JwtPayload
{
{ "userId", user.Id.ToString() },
{ "email", user.Email },
{ "name", user.Name },
{ "exp", DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds() },
};
var token = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(header, payload));// go get github.com/golang-jwt/jwt/v5
claims := jwt.MapClaims{
"userId": fmt.Sprint(user.ID),
"email": user.Email,
"name": user.Name,
"exp": time.Now().Add(30 * time.Minute).Unix(),
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t.Header["kid"] = os.Getenv("GLEAP_IDENTITY_KEY_ID")
token, err := t.SignedString([]byte(os.Getenv("GLEAP_IDENTITY_SECRET")))Return the token to your app, for example in the login response or from an endpoint such as /api/gleap-token. Take the user ID and email from your authenticated server session, never from a value the browser sent.
Sign in from your app
Call setUserJwt after login and on every app start or page load of a logged-in user. The SDK keeps the token in memory only and stores the session grant with the session. setUserJwtProvider is optional: the SDK calls it for a fresh token when the grant is about to run out or a token was refused as expired. Call clearIdentity on logout and before you sign in another user.
await Gleap.setUserJwt(token);
// Optional: fresh tokens for renewal
Gleap.setUserJwtProvider(async () => {
const response = await fetch("/api/gleap-token", { credentials: "include" });
return (await response.json()).token;
});
// On logout
Gleap.clearIdentity();Gleap.setUserJwt(token)
// Optional: fresh tokens for renewal (pass nil when it failed)
Gleap.setUserJwtProvider { completion in
MyBackend.fetchGleapToken { token in completion(token) }
}
// On logout
Gleap.clearIdentity()Gleap.getInstance().setUserJwt(token)
// Optional: fresh tokens for renewal (pass null when it failed)
Gleap.getInstance().setUserJwtProvider(GleapUserJwtProvider { callback ->
myBackend.fetchGleapToken { token -> callback.onToken(token) }
})
// On logout
Gleap.getInstance().clearIdentity()import Gleap from "react-native-gleapsdk";
await Gleap.setUserJwt(token);
Gleap.setUserJwtProvider(async () => await fetchGleapToken());
// On logout
Gleap.clearIdentity();await Gleap.setUserJwt(token: token);
Gleap.setUserJwtProvider(provider: () async => await myBackend.fetchGleapToken());
// On logout
Gleap.clearIdentity();import { Gleap } from "capacitor-gleap-plugin";
await Gleap.setUserJwt({ token });
Gleap.setUserJwtProvider(async () => await fetchGleapToken());
// On logout
Gleap.clearIdentity();You need JavaScript SDK 19.5.0 or newer, or 19.4.0 or newer of the iOS, Android, React Native, Flutter and Capacitor SDKs. The Cordova, .NET MAUI, Unity and Windows SDKs have no setUserJwt yet: pass the token as the signature argument of their identify call (see the user identity page of your SDK). That sign-in is verified, but those SDKs don't send the session grant yet, so their sessions count as unsigned afterwards.
updateContact still sends extra data such as the plan or the language, but it can't change protected fields. setAuthTokens({ backend: accessToken }) gives API tools a token to call your backend as this user; it works only for a signed-in user.
The session grant
The token proves the user once. Gleap then hands back a session grant, signed by Gleap itself, and the SDK sends it with every request from the SDK, the Messenger and the help center. A request with a valid grant is a signed request.
Lifetime. A grant lasts as long as Signed login lasts on the Security page: 7 days by default, 1 to 30 days. The SDK option
sessionDurationcan only shorten it, to at least 5 minutes.The token's
expdoesn't end the session. A one-hour token doesn't log your user out after an hour.Renewal. Before the grant ends, the SDK asks your provider for a fresh token and signs in again, so a chat in progress keeps working. Without a provider, the next
setUserJwtcall renews it.Logout.
clearIdentitydrops the grant on this device. Revoking the key, or shortening Signed login lasts, ends grants everywhere.On the web the grant also lives in the cookie
gleap-grant-<apiKey>, so your help center or feedback portal on the same domain is signed in too.
What a signed request unlocks
Signed fields
The token's claims land on the contact: email, name and phone in the matching fields, company on the company, everything else as custom data. A signed email counts as verified. In the contact view each signed field shows a check, the contact carries a Signed contact badge, and each conversation shows Verified or Unverified. When a later token no longer carries a custom attribute, that value is removed.
Protected fields
Choose which contact fields only signed updates may change: Settings → Inbox & tickets → Data attributes, tab Contact, switch Only signed updates. The user ID is always protected. When you create your first identity key, Gleap protects email, phone, company ID, company name and the billing and shop IDs for you.
A protected field changes only through a signed token, the REST API, the dashboard, a CSV import, a workflow or Kai. A request from the app or the Messenger without a valid token leaves it unchanged and returns no error. An empty protected field may also be filled by the request that creates the contact. Leads (contacts with an email but no user ID) only have the user ID protected, so what a visitor types into the widget form still lands.
Unsigned users and their history
An identified user whose requests carry no valid grant is unsigned: anyone could claim that user ID. Gleap then limits what the session can read. The widget shows only conversations with activity in the last 14 days; older ones stay in Gleap and your team still sees them. Unsigned answers never return protected values or signed data. Visitors and leads are never limited. A signed request, or requiring a signature on that platform, lifts the limit.
Require a signature
Settings → Security → Customer identity → Identity verification has one Require a signature switch per platform (web, iOS, Android), with the week's signed logins next to each. With it on, Gleap refuses an identify without a valid token on that platform with signature_required. Visitors and leads are never refused.
Turn a platform on once every app version you support there sends a token. The platform is what the SDK reports, so turn on every platform you ship to: an unsigned client could otherwise claim to be an app on a platform you left open. Gleap refuses to turn a platform on while no key could sign for it.
Rotate and test keys
Rotate a key. Create a new key, switch your backend to it, then Retire the old one. A retired key still verifies, so tokens already in your users' apps run out normally. When its Last used date stops moving, Revoke it. A revoked key never verifies again and ends the grants it signed. Revoke at once when a secret leaks.
Scope a key. A key for one platform verifies only sign-ins from that platform, so a leaked mobile key can't sign anyone in on the web.
Test a token. Test a token on the Security page checks a token without storing anything: which key matched, the claims, and what the token proves, or why it was refused. Signed logins, last 7 days counts signed sign-ins per platform.
Refused tokens
A bad token is always refused with an error: an app that sends a token wants to be signed, so a broken one never passes as unsigned. The code is in the error body's details.value.
Code | Status | Why | What the SDK does |
|---|---|---|---|
| 401 |
| Asks your provider once and retries. |
| 401 | Wrong signature, unknown or revoked key, wrong shape. | Keeps the session and logs a warning. |
| 401 | The platform requires a signature and none was valid. | Logs the code. |
| 400 | The token is for another user than the current session. | Starts over and signs in again. |
details.reason for token_invalid: key_unknown (no such key, or revoked), no_key_matched (no key verifies the signature), user_mismatch (the token's user ID differs from the identify), key_platform (the key is scoped to another platform) or malformed (not a valid JWT, or no userId).
Call clearIdentity before you sign in another user and identity_mismatch never appears.
Tools that act for the customer
Settings → Security → Customer identity → Customer authentication decides, per channel and audience, when a customer counts as verified for tools that read or change their data: billing, shop, Kai databases, and API and MCP tools.
Signed login: the conversation runs on a signed request. Only the Messenger has one.
Email code: the customer confirms a 6-digit code sent to the email on file (3 tries, valid 10 minutes). On the Messenger through the Verify email card, on email and the chat apps by replying with the code.
By default, identified Messenger users count as verified with a signed login or the email code, and everyone else confirms the code. Each tool has a Verified customers only switch (always on for billing and shop tools). In tools, {{session.verifiedEmail}} holds the proven email, {{session.signedClaims.<name>}} a signed claim, and {{session.authTokens.<name>}} a token from setAuthTokens (signed customers only). In workflows, {{conversation.verification}} is signed_identity, verified_email or unverified, and the Customer verification condition branches on it.
Conversations your backend creates through the server-to-server API count as signed.
Help center, feedback portal and ticket links
They use the same sign-in as the widget, with no second login.
On your own domain, they read the grant cookie the JavaScript SDK set.
Elsewhere, hand the sign-in over:
Gleap.getIdentity()returns{ gleapId, gleapHash, grant }. See Sign visitors in to your help center.Single sign-on. The
ssoendpoints accept an identity token signed with your identity key, withaud: "sso"and anexpat most 10 minutes ahead. Each SSO token works once. Sign a separate token for SSO:identifyrefuses a token withaud: "sso".A session without a grant counts as logged out: it sees no restricted content and goes to your login URL.
Merge contacts
The same person can end up as several contacts: an email to your support address, a website chat before login, the app login. Contacts with their own user ID are never merged into each other.
Case | How |
|---|---|
A visitor chats, then logs in on the same device | Automatic. |
The user logs in with a signed email | Turn on Merge contacts by email on the Security page. Contacts without a user ID that have this email join the user. |
Your server creates the user |
|
Your server decides |
|
When a contact without a user ID writes with the email of an identified user, the inbox shows a warning that the message may not come from that user.
Shopify and WooCommerce
With the Gleap Shopify app or the WooCommerce plugin there's nothing to do: Gleap signs the storefront identities itself, so logged-in shop customers count as signed.

