Konradodocs

Identity verification

Tell the chat which signed-in customer is writing, so Konrado can answer about their own account.

At the end of this page the chat knows which signed-in customer is writing, and Konrado can read that customer's own services, invoices, tickets and hosting setup instead of asking who they are.

How it works

Your server signs a short token for the signed-in customer on every page render and puts it in the chat snippet. The chat hands the token to Konrado once. Konrado checks the signature with the secret key from the console and ties the conversation to that customer. A missing, expired or wrong token gives a normal anonymous chat, never an error.

Only your server and Konrado know the key, so nothing in the browser can forge a customer.

Before you start

  • The chat is installed on a site where customers sign in.
  • You can run code on the server that renders the page. The console writes it for Node.js, PHP and a WHMCS client area, and any language with an HS256 JWT library works. On WordPress the Konrado plugin signs the token for you.
  • A connected help desk. The ID you sign is the customer's ID there. When SupportPal is linked to WHMCS, sign the WHMCS client ID, or the SupportPal ID with platform set to supportpal. Without a help desk, Konrado learns the name, email and ID in the token, with no account behind them.

Steps

Generate the secret key

Open AgentsLive chatInstall and unfold Identity verification under the install steps. Click Generate secret key. Generating a key turns identity verification on, and removing it turns it off.

The Identity verification panel: the masked secret key with Reveal and Copy secret key, the last rotation date, and the warning to treat the key like a password

The key is 43 characters and shows masked, with its last 4 characters visible. The eye button, Reveal, shows it. The copy button, Copy secret key, copies it. The menu next to the key (three dots) holds Rotate and Remove.

Treat the key like a password. Anyone with it can impersonate your customers to the agent. Keep it on your server only, never in your repository or in client-side code.

Sign a token on your server

On every page render, when a customer is signed in, sign a JWT with these claims and the secret key. The algorithm is HS256 and the key is the secret as written, not decoded.

ClaimRequiredValue
subYesThe customer's ID in your help desk, or the WHMCS client ID when SupportPal is linked to WHMCS, unless platform says otherwise. A string of up to 256 characters. Send "1234", not the number 1234: a number is refused
iatYesNow, in whole seconds. A fraction is refused
expYesWhen the token stops working, in whole seconds. The recipes use now + 300: the chat redeems it within seconds of the page load
jtiYesA random string, new on every render. Konrado accepts each jti once
emailNoShown to the agent, up to 320 characters. When there is none, leave it out or send null
nameNoShown to the agent, up to 256 characters. When there is none, leave it out or send null
platformNoWhich system sub comes from, whmcs or supportpal, when one chat runs on WHMCS pages and on SupportPal pages. Without it, Konrado reads sub as the WHMCS client ID when SupportPal is linked to WHMCS, else as the ID in your help desk. With supportpal, it reads sub as the SupportPal client and finds the WHMCS client by email. If it names a system this chat is not connected to, Konrado reads no account. Up to 64 characters. The WHMCS hook sends whmcs. The Node.js and PHP recipes carry the line commented out

Sign exactly that ID, never one from another system, such as your shop's user ID. Konrado reads the account with the ID you sign, so a wrong ID can show the agent another customer's account.

The console writes the code for you. Pick your platform under Choose your platform and click Copy. On PHP and WHMCS the copy carries your real key while the screen shows it masked, so keep the copied file out of your repository.

Uses the jsonwebtoken package. Put the key in the AI_AGENT_IDENTITY_SECRET environment variable, never in the code.

// SERVER - runs on your backend, so the secret never reaches the browser.
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

function konradoIdentityToken(user) {
  const now = Math.floor(Date.now() / 1000);

  return jwt.sign(
    {
      sub: String(user.id),           // your id for this customer
      email: user.email ?? undefined, // optional, shown to the agent
      name: user.name ?? undefined,   // optional, shown to the agent
      // platform: 'supportpal',      // optional: the connected system this id is from
      iat: now,                       // required
      exp: now + 300,                 // 5 minutes - the chat redeems it on page load
      jti: crypto.randomUUID(),
    },
    process.env.AI_AGENT_IDENTITY_SECRET,
    { algorithm: 'HS256' },
  );
}

app.get('/*', (req, res) => {
  // Sign on every page render - the token is short-lived by design.
  res.render('page', { token: req.user ? konradoIdentityToken(req.user) : '' });
});

// TEMPLATE - signed-out visitors render the same tag with an empty token attribute,
// which the loader treats as anonymous.
<script async src="https://app.konrado.ai/website-chat/v1/loader.js"
        data-konrado-chat-id="wgt_9808d383733f4e62925c14e2e876eb3a"
        data-konrado-user-token="{{ token }}"></script>

One block for any PHP site, pasted just before </body>. No Composer package. Swap $_SESSION['user'] for however your app exposes the signed-in user.

<?php
/**
 * Konrado.ai chat - paste this block into your page template,
 * just before the closing </body> tag.
 */

// Your Konrado Chat signing key goes here - the dashboard's Copy button fills it in.
// Leave it empty to embed the chat anonymously.
if (!defined('KONRADO_IDENTITY_SECRET')) {
    define('KONRADO_IDENTITY_SECRET', '');
}

// Your signed-in user - swap this for however your app exposes the current user.
// Leave it null for visitors who aren't logged in: they get the same chat, anonymously.
$konradoUser = $_SESSION['user'] ?? null;

$konradoToken = '';

if (KONRADO_IDENTITY_SECRET !== '' && !empty($konradoUser['id'])) {
    $konradoNow = time();
    $konradoClaims = array_filter([
        'sub'   => (string) $konradoUser['id'],   // your id for this customer
        'email' => $konradoUser['email'] ?? null, // optional, shown to the agent
        'name'  => $konradoUser['name'] ?? null,  // optional, shown to the agent
        // 'platform' => 'supportpal',            // optional: the connected system this id is from
        'iat'   => $konradoNow,                   // required
        'exp'   => $konradoNow + 300,             // 5 min - redeemed on page load
        'jti'   => bin2hex(random_bytes(16)),     // random per render - single-use
    ], static function ($value) {
        return $value !== null;
    });

    // Minimal HS256 JWT signer - no Composer dependency needed. A closure rather than a
    // named function so pasting this block into more than one template can't fatal on a
    // duplicate declaration.
    $konradoEncode = static function ($part) {
        return rtrim(strtr(base64_encode(json_encode($part)), '+/', '-_'), '=');
    };
    $konradoPayload = $konradoEncode(['alg' => 'HS256', 'typ' => 'JWT'])
        . '.' . $konradoEncode($konradoClaims);
    $konradoToken = $konradoPayload . '.' . rtrim(strtr(base64_encode(
        hash_hmac('sha256', $konradoPayload, KONRADO_IDENTITY_SECRET, true)
    ), '+/', '-_'), '=');
}
?>
<script async src="https://app.konrado.ai/website-chat/v1/loader.js"
        data-konrado-chat-id="wgt_9808d383733f4e62925c14e2e876eb3a"
        data-konrado-user-token="<?= htmlspecialchars($konradoToken, ENT_QUOTES) ?>"></script>

A hook file, saved as includes/hooks/konrado_chat.php. WHMCS loads it on its own and prints the tag in the client area. A logged-in client gets a signed token with their WHMCS client ID; everyone else gets the plain tag.

<?php
/**
 * Konrado.ai chat for the WHMCS client area.
 * Upload this file to /includes/hooks/ - WHMCS picks it up automatically.
 */

if (!defined('WHMCS')) {
    die('This file cannot be accessed directly');
}

// Your Konrado Chat signing key goes here - the dashboard's Copy button fills it in.
// Leave it empty to embed the chat anonymously.
define('KONRADO_IDENTITY_SECRET', '');

add_hook('ClientAreaFooterOutput', 1, function (array $vars) {
    $client = $vars['clientsdetails'] ?? [];

    // Signed-out visitors - and installs with no key yet - get the plain tag. Same
    // install, no second snippet.
    if (KONRADO_IDENTITY_SECRET === '' || empty($client['id'])) {
        return '<script async src="https://app.konrado.ai/website-chat/v1/loader.js"
        data-konrado-chat-id="wgt_9808d383733f4e62925c14e2e876eb3a"></script>';
    }

    $now = time();
    $name = trim(($client['firstname'] ?? '') . ' ' . ($client['lastname'] ?? ''));
    $token = konrado_identity_token([
        'sub'   => (string) $client['id'],   // your id for this customer
        'email' => $client['email'] ?? null, // optional, shown to the agent
        'name'  => $name !== '' ? $name : null, // optional, shown to the agent
        'platform' => 'whmcs',               // the system this id belongs to
        'iat'   => $now,                     // required
        'exp'   => $now + 300,               // 5 min - redeemed on page load
        'jti'   => bin2hex(random_bytes(16)),
    ]);

    return str_replace('{{ token }}', htmlspecialchars($token, ENT_QUOTES), '<script async src="https://app.konrado.ai/website-chat/v1/loader.js"
        data-konrado-chat-id="wgt_9808d383733f4e62925c14e2e876eb3a"
        data-konrado-user-token="{{ token }}"></script>');
});

if (!function_exists('konrado_identity_token')) {
    /** Minimal HS256 JWT signer - no Composer dependency needed. */
    function konrado_identity_token(array $claims): string
    {
        $encode = static function ($part) {
            return rtrim(strtr(base64_encode(json_encode($part)), '+/', '-_'), '=');
        };

        $payload = $encode(['alg' => 'HS256', 'typ' => 'JWT'])
            . '.' . $encode(array_filter($claims, static function ($value) {
                return $value !== null;
            }));
        $signature = hash_hmac('sha256', $payload, KONRADO_IDENTITY_SECRET, true);

        return $payload . '.' . rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
    }
}

No code. In WordPress open SettingsKonrado AI, tick Recognize customers who are logged in to WordPress and paste the key into Signing secret. The plugin signs the token for every logged-in user. Out of the box it sends wp: and the WordPress user ID as sub, which matches no help desk client, so the agent knows the name and email and not the account. Set up the WordPress plugin shows how to map a user to a help desk client.

Put the token in the tag

The tag carries the token in data-konrado-user-token. The chat reads it once, when the page loads, and never puts it in a URL. For a signed-out visitor render the tag with an empty attribute, or with no attribute at all: both give the anonymous chat.

Check it works

Sign in to your site as a test customer, open the chat and ask "which plan am I on". Konrado names the service. In the Inbox the conversation's details show the customer's name, their email with signed in, and Client # with their ID, linked to your help desk.

What changes for a verified visitor

WhatAnonymous visitorVerified visitor
Who the agent seesNothing, until the visitor says soName, email and the customer ID. The agent can greet them by name
Account dataNoneThe customer's services, invoices, tickets and profile, read-only. On SupportPal, tickets and profile. When SupportPal is linked to WHMCS, services and invoices come from WHMCS. A WHMCS client with no SupportPal client gets only those two: no tickets, profile or own service
Their own serviceNoneWith a server integration connected, Konrado reads the customer's own service on your server, read-only: PHP version, website and domain settings, email accounts, databases, disk use, backups and error logs. It also runs every Hosting diagnostics check on that service, the four marked Signed-in visitors only included, even when the action is off
Lead formOffered when the visitor is worth following upNever. A customer is not a lead
Escalation ticketOpened on the email the visitor typesOpened on the customer's own account, with name and email filled in. Departments marked Logged in are offered
TriggersEvery audience but oneAlso triggers aimed at Signed-in customers
In the InboxA plain avatarA check mark on the avatar, signed in next to the email, Client # linked to your help desk, and the Signed in filter

Log out and switching customers

On every page load the chat compares the token with the session it already has in that browser tab. A missing platform counts as a value of its own. So to the chat, a token with sub: "5", platform: "whmcs" and a token with sub: "5" and no platform are two customers.

The page rendersWhat the chat does
No token, after a sign-outDrops the signed-in session and starts a new anonymous conversation
A token with another sub or another platformStarts a new session and a new conversation
A token with the same sub and the same platformKeeps the session. The token is not redeemed again, so a reused jti is no problem here
A token where there was noneUpgrades the session. The conversation continues, now verified

With or without a help desk, the next person on the same browser cannot open the previous customer's conversations. When that customer signs in again, their conversations are back under Recent chats. A conversation from before 13 October 2026 whose customer had no help desk account stays with the browser until that customer writes in it again while signed in. A chat session lasts 3 hours in a tab.

Rotate and remove the key

  • Rotate generates a new key at once. The previous key keeps working until the next rotation, so deploy the new key before you rotate again. If the old key may have leaked, deploy the new key, rotate once more and deploy that key too: the leaked key stops working at the second rotation.
  • Remove turns identity verification off. New sessions are anonymous and read no account data. A session verified before keeps its customer until it ends, up to 3 hours.

What Konrado cannot do

Not possibleWhy
Verify a customer from a plain HTML snippetThe tag cannot sign anything. The signing needs your server, or the WordPress plugin
Report a bad tokenA wrong or expired token gives the anonymous chat, with no error to the visitor or in the console. Check the Inbox instead
Change the customer's accountAccount data is read-only. Changes stay in your help desk and your panel

If something is wrong

What you seeWhat to do
A signed-in customer chats as a guestOpen the page source: data-konrado-user-token must hold a token. Check that your server has the current key, that sub is a string, that iat and exp are whole seconds and that jti is present. Your server's clock must not be more than 5 minutes slow, or every token arrives expired. A token over 4096 characters is dropped.
Customers chat as guests on some pages onlyThose pages come from a page cache, so they carry an old token that was already used or has expired. Each token works once: keep signed-in pages out of the cache.
A signed-in customer's conversation starts over when they open another pageThat page signs them with another sub or another platform, for example platform on your WHMCS pages and none on the others. The chat takes that for another customer. Sign the same sub and the same platform on every page.
The agent knows the name but not the accountsub is not the customer's ID in your help desk, or not the WHMCS client ID when SupportPal is linked to WHMCS. If you send platform, it must name a system the chat is connected to. WordPress sends wp: and the user ID unless you map it.
With SupportPal linked to WHMCS, only part of the account reaches the agentThe other system finds the customer by email: exactly one client there must carry the email of the client you signed. Without platform you sign the WHMCS client, so tickets and the profile need that match. With platform set to supportpal, services and invoices need it.
The Inbox shows no signed in markNo help desk is connected to the agent, or platform names a system the chat is not connected to. The agent still has the name and email from the token, and the next person on the browser still cannot open the chat.
Recognition stopped after a second rotationYour server signs with a key two rotations old. Copy the current key from Identity verification and deploy it.

Last updated on

On this page