Signed identity

A plain identify call comes from the browser, so anyone can change it. Signed identity fixes that. Your server creates a short-lived JWT that proves who the user is, and the widget sends it to UserJot.

With a signed identity, everything in the token is verified: the user ID, email, name, traits, and companies. Nothing can be spoofed from the browser.

When you need this

  • You want to trust that feedback and messages really come from the user they claim to come from.
  • You enabled Require signed tokens in your workspace settings. Unsigned identify calls are then rejected.
  • Workspace admins and owners always need a signed identity. Unsigned identification is never accepted for privileged accounts.

1. Create the token on your server

Sign a JWT with the HS256 algorithm using your workspace secret. You can find both values in SettingsLogin, in the Secrets section, listed as Project ID and Project secret.

Never sign tokens in the browser. The secret must stay on your server.

The token must include these claims:

ClaimValue
subThe user's ID in your system. Required.
issYour UserJot project ID. Required.
audThe exact string userjot. Required.
iatIssued at, in Unix seconds. Required.
expExpiry, in Unix seconds. Required. Maximum 1 hour after iat.

You can also include these optional claims: email, firstName, lastName, avatar, traits, and companies. Anything you put in the token is treated as verified.

import jwt from 'jsonwebtoken';
 
export function createUserJotToken(user, secret, projectId) {
  return jwt.sign(
    {
      sub: user.id,
      email: user.email,
      firstName: user.firstName,
      lastName: user.lastName,
      iss: projectId,
      aud: 'userjot'
    },
    secret,
    { algorithm: 'HS256', expiresIn: '1h' }
  );
}

Create a fresh token on each page load, for example when you render the page or through a small endpoint your frontend calls. Tokens are capped at 1 hour, so do not cache them for longer.

2. Pass the token to identify

Send the token in the token field:

uj.identify({
  token: 'TOKEN_FROM_YOUR_SERVER'
});

You can still pass user and companies alongside the token. They are used as extra, unverified detail. If a field appears in both, the token wins:

uj.identify({
  token: 'TOKEN_FROM_YOUR_SERVER',
  user: { id: 'user-123' },
  companies: [{ id: 'acme', name: 'ACME Inc.' }]
});

You can confirm the identity was verified from the result:

const result = await uj.identify({ token });
result.identityTrust; // 'signed'

Invalid tokens fail, they do not fall back

If the token is expired, malformed, or signed with the wrong secret, the identify call rejects with IDENTIFY_FAILED. It does not silently continue as an unsigned identity. This is deliberate: a broken signing setup should be loud, not invisible.

Signed identity and conversations

Signed identity is what attaches widget conversations to a verified account. Two behaviors are worth knowing:

  • If a visitor starts a conversation anonymously and then identifies with a signed token, their guest conversation is handed off to the member account, so history follows them.
  • A conversation started under a signed identity stays protected. Opening it later without a signed session shows "Verify to view this conversation" until they identify again with a signed token.

Requiring signed tokens does not close the door on anonymous visitors: they can still start guest conversations scoped to their browser. What strict mode changes is that a conversation can only be attached to a member account through a signed token — unsigned identify calls are rejected outright.

Identity carries over to your public pages

Once a user is identified, the SDK automatically attaches the identity to links from your product that point to your public UserJot pages, like your board, roadmap, or changelog. For the user to land there already signed in, your workspace also needs Automatic login enabled in settings — that is the mechanism the public pages use to accept the identity. Without it, the links still work but the user arrives signed out.

Troubleshooting

The identify call rejects with IDENTIFY_FAILED

Check these:

  • The token is signed with HS256 and your current workspace secret.
  • iss matches your project ID and aud is exactly userjot.
  • Both iat and exp are present, in Unix seconds.
  • The lifetime is 1 hour or less. Longer tokens are rejected.
  • Your server clock is accurate. Tokens issued in the future are rejected, with 60 seconds of allowed clock skew.

Unsigned identify stopped working

That usually means Require signed tokens was enabled for your workspace. You can find it in SettingsLogin, where it appears once Automatic login is on. Once it is enabled, every identify call needs a token.