---
title: "Enforce identity verification"
description: "Protect customer identities in Gleap with server-generated user hashes. Learn how to require identity verification across SDK platforms."
url: https://docs.gleap.ai/en/articles/709-enforce-identity-verification
collection: "Guides"
updated: 2026-11-01
language: en
---

# Enforce identity verification

Protect customer identities in Gleap with server-generated user hashes. Learn how to require identity verification across SDK platforms.

Enforce identity verification on all platforms to prevent third parties from impersonating logged-in users. Once enabled, Gleap will require you to pass the correct user hash in order to identify users.

For private conversation attachments, also follow [Authenticated conversation files](https://gleap-developer-docs.gleap.help/en/articles/710-authenticated-conversation-files). That guide covers the compatible SDK, customer application URL and preserving attachment links through login. Identity verification alone does not enable protected file storage.

## User hash generation

To set up identity verification, you'll need to generate an [HMAC](https://en.wikipedia.org/wiki/HMAC) on your server for each logged-in user and send it to Gleap.

Please choose your server stack to show an example code for the user hash generation.

**NodeJS**

```js
const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'YOUR-SECRET'); // secret key (keep it safe!)
const userIdAsString = String(user.id); // convert user's id to string
const userHash = hmac.update(userIdAsString).digest('hex'); // generate hash
```

**Rails (Ruby)**

```rails
OpenSSL::HMAC.hexdigest(
'sha256', # hash function
'YOUR-SECRET', # secret key (keep safe!)
current_user.id.to_s # user's id
)
```

**Django (Python 3)**

```py
import hmac
import hashlib

hmac.new(
b'YOUR-SECRET', # secret key (keep it safe!)
bytes(request.user.id, encoding='utf-8'), # user's id
digestmod=hashlib.sha256 # hash function
).hexdigest()
```

**PHP**

```php
$userHash = hash_hmac(
'sha256', // hash function
$user->id, // user ID
'YOUR-SECRET' // secret key (keep it safe!)
);
```

> **Info:** Important: Gleap handles the userId as STRING, please make sure to convert the user ID to a string, before generating the hash.

> **Warning:** Keep your secret key safe! Never commit it directly to your client-side code, or anywhere a third party can find it.

## Signed identity token (proves the email too)

The user hash above proves the **user ID** only. If you also sign the user's **email**, Gleap knows the email really belongs to this user. That is what lets Gleap bring the person's earlier conversations together (see [Merge contacts](#merge-contacts)).

Instead of the user hash, create a JSON Web Token (HS256) with the same secret key and pass it in the same place. Every SDK accepts it as the user hash, so no SDK update is needed, and existing app versions keep working.

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Claim</p></th><th colspan="1" rowspan="1"><p></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>user_id</code></p></td><td colspan="1" rowspan="1"><p>Required. The user ID you pass to identify, as a string. <code>userId</code> is accepted too.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>email</code></p></td><td colspan="1" rowspan="1"><p>Optional. Only put an email in the token that your system has confirmed (sign-up confirmation, SSO). Signing it means you vouch for it.</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>exp</code></p></td><td colspan="1" rowspan="1"><p>Optional. Once the token has expired it still identifies the user, but no longer proves the email.</p></td></tr></tbody></table>

**NodeJS**

```js
const jwt = require('jsonwebtoken');

const userHash = jwt.sign(
  { user_id: String(user.id), email: user.email },
  'YOUR-SECRET', // secret key (keep it safe!)
  { algorithm: 'HS256' }
);
```

**PHP**

```php
use Firebase\JWT\JWT; // composer require firebase/php-jwt

$userHash = JWT::encode(
  ['user_id' => (string) $user->id, 'email' => $user->email],
  'YOUR-SECRET', // secret key (keep it safe!)
  'HS256'
);
```

**Python**

```py
import jwt  # pip install PyJWT

user_hash = jwt.encode(
    {"user_id": str(user.id), "email": user.email},
    "YOUR-SECRET",  # secret key (keep it safe!)
    algorithm="HS256",
)
```

**Rails (Ruby)**

```rails
# gem 'jwt'
JWT.encode(
  { user_id: current_user.id.to_s, email: current_user.email },
  'YOUR-SECRET', # secret key (keep safe!)
  'HS256'
)
```

**C# (.NET)**

```csharp
// dotnet add package System.IdentityModel.Tokens.Jwt
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YOUR-SECRET"));
var token = new JwtSecurityToken(
    claims: new[] { new Claim("user_id", user.Id.ToString()), new Claim("email", user.Email) },
    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
var userHash = new JwtSecurityTokenHandler().WriteToken(token);
```

Then pass the token as the user hash, with **the same email** in the user data:

```js
Gleap.identify("user_19283", { name: "Franz", email: "franz@gleap.ai" }, "SIGNED_IDENTITY_TOKEN");
```

-   The token works everywhere the user hash works, including enforced identity verification and authenticated conversation files.
    
-   A token whose email differs from the email you send identifies the user, but proves no email.
    
-   An email sent without a token is saved as before, but never counts as proven.
    

> **Info:** The classic user hash keeps working. You can switch to the token at any time, user by user.

## Merge contacts

The same person can end up as several contacts: an email to your support address creates one, a website chat without login another, your app login a third. Gleap brings them together in these cases. Contacts with their own user ID are never merged into each other.

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Case</p></th><th colspan="1" rowspan="1"><p>How</p></th></tr><tr><td colspan="1" rowspan="1"><p>Same device: a visitor chats, then logs in</p></td><td colspan="1" rowspan="1"><p>Automatic, always on.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Your server creates the user</p></td><td colspan="1" rowspan="1"><p><code>POST /admin/identify</code> with the user ID and email takes over the single contact that only has this email (for example from an earlier email).</p></td></tr><tr><td colspan="1" rowspan="1"><p>The user logs in with a signed email</p></td><td colspan="1" rowspan="1"><p>Turn on <strong>Settings → User identity → Merge contacts by email</strong>. Every contact without a user ID that has this email joins the user, once per user and email. Off by default.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Your server decides</p></td><td colspan="1" rowspan="1"><p><code>POST /admin/contacts/merge</code> merges a contact without a user ID into a user. See the <a target="_blank" rel="noopener noreferrer nofollow" class="linktype-link link" data-linktype="link" href="https://gleap-developer-docs.gleap.help/en/articles/708-rest#merge-contacts">REST API</a>.</p></td></tr><tr><td colspan="1" rowspan="1"><p>A website visitor confirms the email with a code</p></td><td colspan="1" rowspan="1"><p>The "Verify email" workflow step. If a user with this email exists and your server or a signed token proved that email, the chat moves to the user.</p></td></tr></tbody></table>

Conversations, messages and survey answers move to the user. Details the user doesn't have yet, such as the company, only come from contacts your server or an inbound email created.

## Identify user with user hash

After generating the user hash you need to send it to your client and pass it to the Gleap SDK.

**JS**

```js
Gleap.identify("user_19283", {
  name: "Franz",
  email: "franz@gleap.ai",
}, "GENERATED_USER_HASH");
```

**iOS**

```objc
let userProperty = GleapUserProperty()
userProperty.name = "Franz"
userProperty.email = "franz@gleap.ai"
Gleap.identifyUser(with: "user_1234", andData: userProperty andUserHash: "GENERATED_USER_HASH")
```

**Android**

```java
GleapUserProperties userProperties = new GleapUserProperties();
userProperties.setEmail("franzi@gleap.ai");
userProperties.setName("Franzi");
userProperties.setPhone("+1 (902) 123123");
userProperties.setValue(199.95);
userProperties.setHash("GENERATED_USER_HASH");
Gleap.getInstance().identifyUser("12334", userProperties);
```

**ReactNative**

```js
Gleap.identifyWithUserHash('12334', {
  name: 'Franz',
  email: 'franz@gleap.ai',
}, 'GENERATED_USER_HASH');
```

**Flutter**

```js
Gleap.identifyWithUserHash(
    userId: '12345',
    userProperties: GleapUserProperty(name: 'Franz', email: 'franz@gleap.ai'),
    'GENERATED_USER_HASH',
);
```

## Related

- [Authenticated conversation files](https://docs.gleap.ai/en/articles/710-authenticated-conversation-files.md)
- [Data regions](https://docs.gleap.ai/en/articles/711-data-regions.md)
- [Connect the Gleap MCP](https://docs.gleap.ai/en/articles/712-connect-the-gleap-mcp.md)
- [Plans and AI pricing](https://docs.gleap.ai/en/articles/713-plans-and-ai-pricing.md)
- [Screenshots, recordings and logs](https://docs.gleap.ai/en/articles/714-screenshots-recordings-and-logs.md)
