TrueEngageDocsStart free

Identity verification

Sign each user's ID on your server so TrueEngage trusts who the SDK says is using your app.

Updated 23 Oct 2026 7 min read

Identity verification makes the SDK prove who a user is. Your server signs the user's ID, the app passes the signed token to the SDK, and TrueEngage trusts the ID only when the signature checks out.

Why you need it

The SDK key is public. It ships inside your app or website, so anyone can copy it. Without verification, anyone holding the key can send any externalId and be treated as that person. They could read that person's cards, post results against them or probe which audiences they are in.

For this reason, in-app, on-site and card messages show only the fallback text of personalisation tokens until you turn verification on. See Personalise a template.

How it works

  1. Your backend signs a short-lived token (a JWT) that names the signed-in user.
  2. Your app gets the token from your backend and gives it to the SDK.
  3. The SDK sends the token in an X-TE-Identity header with its in-app and card requests.
  4. TrueEngage checks the signature with a secret only you and TrueEngage hold. If it is valid, the user is verified.

Verification is set per environment, so Test and Live are switched on separately. It is off until you turn it on, and apps that don't use it keep working unchanged.

What verification covers today:

  • Covered: fetching in-app messages and cards, reporting their results (shown, clicked, dismissed), and changing card state.
  • Not covered yet: event tracking and identify calls (/sdk/batch and /sdk/identify). These still trust the externalId the SDK sends.

Turn it on

  1. Open Settings, then Identity verification, and pick the environment (Test or Live).
  2. Click Create secret. Copy it and store it in your backend's secret manager. It is shown once.
  3. Add signing to your backend and the token to your app (below). Release the app.
  4. Switch Identity verification on.

Do step 4 last. Once it is on, an externalId without a valid token is treated as anonymous. If your released apps don't send tokens yet, in-app messages and cards stop reaching signed-in users.

Managing secrets needs the same permission as API keys. See API keys.

Secrets

  • A secret is generated by TrueEngage. You can't choose it.
  • It is shown once, when you create it. Lose it and you create a new one.
  • Each secret has a key ID (kid).
  • Up to two secrets are active at once, so you can rotate without downtime.
  • You retire a secret yourself. While verification is on, you can't retire the last active one.
  • Use the secret string exactly as shown as the HMAC key. Don't decode it.

Rotate a secret

  1. Create a second secret. Now two are active and either one verifies.
  2. Switch your backend to sign with the new secret.
  3. Wait until tokens signed with the old secret have expired (24 hours at most, or your own token lifetime).
  4. Retire the old secret.

Changes take up to about a minute to apply everywhere.

The token

A standard JWT, signed with HS256.

Part Rule
Algorithm HS256 only. Any other algorithm, including none, is rejected.
Header kid Optional. Set it to the key ID of the secret you signed with. Without it, TrueEngage tries every active secret.
sub Required. The user's externalId, 1 to 256 characters.
exp Required. At most 24 hours from now.
iat Optional. Must not be more than 60 seconds in the future.

TrueEngage allows 60 seconds of clock difference. Keep tokens short-lived, such as 1 hour, and issue a fresh one when needed.

When a token is valid, the user is whoever sub names. An externalId in the same request that differs from sub is ignored.

Sign the token on your server

Sign on your server only. Never put the secret in your app or website, because anyone could then sign for any user.

Node (jsonwebtoken):

import jwt from 'jsonwebtoken';

const token = jwt.sign({ sub: user.id }, process.env.TE_IDENTITY_SECRET, {
  algorithm: 'HS256',
  expiresIn: '1h',
  keyid: process.env.TE_IDENTITY_KID, // optional
});

Python (PyJWT):

import os, time, jwt

token = jwt.encode(
    {"sub": user_id, "exp": int(time.time()) + 3600},
    os.environ["TE_IDENTITY_SECRET"],
    algorithm="HS256",
    headers={"kid": os.environ["TE_IDENTITY_KID"]},  # optional
)

PHP (firebase/php-jwt):

use Firebase\JWT\JWT;

$token = JWT::encode(
    ['sub' => $userId, 'exp' => time() + 3600],
    getenv('TE_IDENTITY_SECRET'),
    'HS256',
    getenv('TE_IDENTITY_KID') // optional
);

Java (java-jwt):

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;

String token = JWT.create()
    .withKeyId(System.getenv("TE_IDENTITY_KID")) // optional
    .withSubject(userId)
    .withExpiresAt(new Date(System.currentTimeMillis() + 3_600_000))
    .sign(Algorithm.HMAC256(System.getenv("TE_IDENTITY_SECRET")));

Go (golang-jwt/jwt):

import (
	"os"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
	"sub": userID,
	"exp": time.Now().Add(time.Hour).Unix(),
})
t.Header["kid"] = os.Getenv("TE_IDENTITY_KID") // optional
token, err := t.SignedString([]byte(os.Getenv("TE_IDENTITY_SECRET")))

Expose an endpoint in your backend that returns a token for the signed-in user, and have your app call it.

Give the token to the SDK

Pass the token when you identify the user, set it later, or give the SDK a provider that returns a fresh token. A provider is the better choice. The SDK calls it when it has no token, when the token is within 60 seconds of expiring, and once after an IDENTITY_TOKEN_EXPIRED response, then retries the request once.

The SDK sends the token only with in-app and card requests, never logs it, and clears it on logout or reset.

Web

// With identify
TrueEngage.identify('u-1001', { firstName: 'Priya' }, { token });

// Or set it separately
TrueEngage.setIdentityToken(token);

// Or let the SDK ask for a fresh one
TrueEngage.setIdentityTokenProvider(async () => {
  const res = await fetch('/api/te-token');
  return (await res.json()).token;
});

Android

TrueEngage.identify("u-1001", mapOf("firstName" to "Priya"), token)

TrueEngage.setIdentityToken(token)

TrueEngage.setIdentityTokenProvider(object : TEIdentityTokenProvider {
    override fun fetchToken(): String? = myBackend.fetchTeToken()
})

iOS

TrueEngage.identify("u-1001", attributes: ["firstName": "Priya"], token: token)

TrueEngage.setIdentityToken(token)

TrueEngage.setIdentityTokenProvider { await myBackend.fetchTeToken() }

Flutter

TrueEngage.identify('u-1001', {'firstName': 'Priya'}, token);

TrueEngage.setIdentityToken(token);

TrueEngage.setIdentityTokenProvider(() async => await myBackend.fetchTeToken());

The provider is a Future<String?> Function().

What changes when it is on

Request Verification off Verification on
Valid token Ignored. The user is sub. Their real profile is used for personalisation.
externalId, no token Trusted as the user. Ignored. The request is treated as anonymous, using anonymousId if one is sent.
Invalid token Ignored. Rejected with 401 and IDENTITY_TOKEN_INVALID.
Expired token Ignored. Rejected with 401 and IDENTITY_TOKEN_EXPIRED.
Anonymous visitor (anonymousId only) Works. Works.

When it is off, profile fields never render in in-app and card messages, and a token has no effect.

Personalisation

For a verified user, {{ user.first_name }} and the other profile variables render with the real values. Everyone else, including anonymous visitors and users without a valid token, still gets the fallback text, so write a default for each token. See In-app messages and cards.

A verified user must exist in TrueEngage under that externalId. A user matched only through anonymousId is not treated as verified.

Troubleshooting

In-app messages or cards stopped showing for signed-in users. Verification is on and the app isn't sending a valid token, so the user is treated as anonymous. Check that the app calls identify with a token, setIdentityToken or a provider, and that the app version with that code is released. Turn verification off while you fix it.

401 with IDENTITY_TOKEN_INVALID. The signature or claims failed. Check:

  • The algorithm is HS256.
  • The secret is the exact string shown, from an active secret in the same environment (Test and Live have separate secrets).
  • sub is present and no longer than 256 characters.
  • exp is present and less than 24 hours ahead.
  • If you set kid, it matches an active secret.
  • The secret hasn't been retired.

401 with IDENTITY_TOKEN_EXPIRED. The token's exp has passed. Use a token provider so the SDK refreshes it. If tokens expire straight away, check that your server's clock is correct.

Valid token, but personalisation still shows fallbacks. The user must exist in TrueEngage under the externalId in sub, and the property must be on their profile. A user known only by anonymousId doesn't render real values.

Changes don't take effect straight away. Enabling, disabling and retiring can take up to about a minute.

I lost my secret. Create a new secret, update your backend, then retire the old one. See Rotate a secret.

Limits

  • Tracking events and identify calls aren't verified yet. Don't rely on verification to stop forged events.
  • Only HS256 is supported.
  • There is no per-user revoke. Short expiry and secret rotation are how you cut off access.

What happens next

Add {{ user.* }} variables to your in-app templates and test them with a signed-in user. See In-app messages and cards. For identify and events, see Events and identity.