Mobile SDK keys and identifying users (user_hash)

Last updated: 9 October 2026 · 9 min read

Without a login, the Messenger opens for an anonymous visitor. When a user signs in to your app, tell Clomni who they are: agents then see their name, e-mail and attributes, the user sees the same conversations on every device, and what they wrote as a visitor moves to them.

Steps

  1. 1

    The keys and where they live

    • App ID (app_…): in the app.
    • Android API key (android_…): in the Android app.
    • iOS API key (ios_…): in the iOS app.
    • Identity Secret (a long random string): only on your server. Not in the code, not in BuildConfig, not in Info.plist, not in a .env shipped with the app, not in Unity assets.

    An API key belongs to one platform: the iOS key does not work on Android. React Native, Flutter and Unity apps pass the key of the platform they run on. The keys are in Settings › Workspace settings › Inboxes › your channel › settings column › Installation, the Identity Secret in the Security section.

  2. 2

    How it works

    1. The user signs in to your app.
    2. Your server computes a user_hash from the user's ID with the Identity Secret and returns it to the app, for example in the login response.
    3. The app calls loginUser with the user and the hash.

    The hash proves your server vouches for this ID. Without it, anyone who knows a user's ID could open that user's conversations from their own phone.

  3. 3

    Formula

    user_hash = hex(HMAC-SHA256(identity_secret, user_id))
    • The result is 64 lower-case hex characters.
    • user_id is a string, exactly the one the app passes to loginUser. 12345 and "12345" are the same, "012345" is not.
    • When the app signs a user in without userId, the hash is made from the e-mail, trimmed and in lower case.
    • Use an ID that never changes. If the e-mail can change, do not use it as the ID.

    To test your code: with the secret test_secret and user_id 12345, the hash must be b01354f5d4c60c56ca76a26c064b9629b0d2a73e169b63b6514b974a9714cf24.

  4. 4

    Server code

    Each sample reads the secret from the CLOMNI_IDENTITY_SECRET environment variable.

    // Node.js
    const crypto = require('crypto');
    
    function clomniUserHash(userId) {
      return crypto
        .createHmac('sha256', process.env.CLOMNI_IDENTITY_SECRET)
        .update(String(userId))
        .digest('hex');
    }
    
    // Return the hash with your own login response (Express).
    app.post('/api/login', async (req, res) => {
      const user = await logIn(req.body.email, req.body.password); // your own login
      res.json({ user, clomniUserHash: clomniUserHash(user.id) });
    });
    <?php
    // PHP
    function clomni_user_hash(string $userId): string
    {
        return hash_hmac('sha256', $userId, getenv('CLOMNI_IDENTITY_SECRET'));
    }
    # Python
    import hashlib
    import hmac
    import os
    
    def clomni_user_hash(user_id) -> str:
        secret = os.environ["CLOMNI_IDENTITY_SECRET"].encode()
        return hmac.new(secret, str(user_id).encode(), hashlib.sha256).hexdigest()
    # Ruby
    require 'openssl'
    
    def clomni_user_hash(user_id)
      OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('CLOMNI_IDENTITY_SECRET'), user_id.to_s)
    end
    // Java
    import java.nio.charset.StandardCharsets;
    import java.security.GeneralSecurityException;
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    
    public final class ClomniUserHash {
        private ClomniUserHash() {}
    
        public static String of(String userId) throws GeneralSecurityException {
            String secret = System.getenv("CLOMNI_IDENTITY_SECRET");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] digest = mac.doFinal(userId.getBytes(StandardCharsets.UTF_8));
            StringBuilder hex = new StringBuilder(digest.length * 2);
            for (byte b : digest) {
                hex.append(String.format("%02x", b));
            }
            return hex.toString();
        }
    }
    // C# (.NET 5 or newer)
    using System;
    using System.Security.Cryptography;
    using System.Text;
    
    public static class ClomniUserHash
    {
        public static string Of(string userId)
        {
            var secret = Environment.GetEnvironmentVariable("CLOMNI_IDENTITY_SECRET")!;
            using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
            var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(userId));
            return Convert.ToHexString(digest).ToLowerInvariant();
        }
    }
    // Go
    package clomni
    
    import (
    	"crypto/hmac"
    	"crypto/sha256"
    	"encoding/hex"
    	"os"
    )
    
    // UserHash is the user_hash Clomni expects for userID.
    func UserHash(userID string) string {
    	mac := hmac.New(sha256.New, []byte(os.Getenv("CLOMNI_IDENTITY_SECRET")))
    	mac.Write([]byte(userID))
    	return hex.EncodeToString(mac.Sum(nil))
    }

    In C#, Convert.ToHexString returns upper case, so ToLowerInvariant is needed.

  5. 5

    Login, changes and logout in the app

    Call loginUser after your own login succeeds, with the hash from your server. Every field except userId is optional, and there is phone too.

    // Android
    Clomni.loginUser(ClomniUser(userId = "12345", email = "[email protected]", name = "Aysel"), userHash = hashFromYourServer)
    Clomni.updateUser(language = "az", customAttributes = mapOf("plan" to "premium"))
    Clomni.logout()
    // iOS
    Clomni.loginUser(ClomniUser(userId: "12345", email: "[email protected]", name: "Aysel"), userHash: hashFromYourServer)
    Clomni.updateUser(language: "az", customAttributes: ["plan": "premium"])
    Clomni.logout()
    // React Native
    Clomni.loginUser({ userId: '12345', email: '[email protected]', name: 'Aysel' }, hashFromYourServer);
    Clomni.updateUser({ language: 'az', customAttributes: { plan: 'premium' } });
    Clomni.logout();
    // Flutter
    await Clomni.loginUser(
      const ClomniUser(userId: '12345', email: '[email protected]', name: 'Aysel'),
      userHash: hashFromYourServer,
    );
    await Clomni.updateUser(language: 'az', customAttributes: {'plan': 'premium'});
    await Clomni.logout();
    // Unity
    Clomni.LoginUser(new ClomniUser { UserId = "12345", Email = "[email protected]", Name = "Aysel" }, hashFromYourServer);
    Clomni.UpdateUser(language: "az", customAttributes: new Dictionary<string, object> { ["plan"] = "premium" });
    Clomni.Logout();
    • updateUser changes only the fields given: name, language (az, en, ru), custom attributes. The attributes are merged into the ones the agent sees in the conversation's side panel.
    • Call logout together with your app's own logout. It closes the Clomni session and deletes the Messenger's data on the device. Without it, the next person who signs in on this phone sees the previous user's conversations.

    Call loginUser on every app start while the user is signed in, as if they had just signed in. For the same user the SDK reuses its stored session.

  6. 6

    Modes

    The mode is in settings column › Security › “Mode”.

    • “Off”: the hash is not checked, any hash is accepted. Anyone who gives the app another user's user_id can open their conversations. For testing only: never use it in production.
    • “Recommended” (the default): a hash that is sent must be correct, a wrong hash refuses the login. A user without a hash is accepted but not verified. Once a user has signed in with a verified hash, a hash is required for them every time.
    • “Enforced”: no login without a correct hash, the server answers 403 identity_verification_failed. It can be chosen only after the app's first verified login. Older app versions that send no hash cannot connect.

    A good order: release the app with user_hash in “Recommended” mode, watch “Overview” until no hash warning is left, then switch to “Enforced”.

  7. 7

    Test a hash

    Side menu › Security › “Test a hash”: enter the user_id and the user_hash your server made, then click “Check”. The dashboard says whether the hash was made with the current secret, with the old secret, or matches neither. The right answer is “Correct. The hash was made with the current secret.” If the hash is not 64 hex characters, the dashboard says so.

  8. 8

    Changing the secret and the keys

    • Security › “Make a new secret”: the new secret is shown once, the old one keeps working for 7 more days (the dashboard shows the date). Move your server to the new secret within that time.
    • Installation › “New key”: a new API key, the old one keeps working for 7 more days. Make a new key only when the old one leaked, or together with a release most users will install.
    • Installation › “Revoke”: the key stops at once.
  9. 9

    Full guide

Document