Skip to content

Documentation Index

Fetch the complete documentation index at: /llms.txt

Use this file to discover all available pages before exploring further.

Identity verification

Pass server-signed user information and choose the right verification method for your chat window.

Open copy menu
View as Markdown

Without identity verification, a visitor can supply any user ID in an identify call. Sign identity data on your server only after checking the user’s authenticated session. Aihio checks that signature, not your application’s sign-in process.

Aihio supports two methods: HMAC user-hash and JWT HS256 token.

Open the chatbot’s Settings → Security page in the dashboard. In the Identity verification section, press Create key and copy the value immediately: it is shown only once. The secret looks like aihio_idv_….

Store it in your server-side environment (e.g. AIHIO_IDENTITY_SECRET). Never include it in client-side JavaScript or commit it to version control.

Compute HMAC-SHA256(secret, user_id) on your server and pass the result as the user_hash field. The output must be lowercase hex. Aihio rejects uppercase hex.

All examples below produce the correct format by default.

const crypto = require('crypto');
function computeUserHash(secret, userId) {
return crypto.createHmac('sha256', secret).update(userId).digest('hex');
}
import hmac, hashlib
def compute_user_hash(secret: str, user_id: str) -> str:
return hmac.new(
secret.encode('utf-8'),
user_id.encode('utf-8'),
hashlib.sha256
).hexdigest()
function compute_user_hash(string $secret, string $user_id): string {
return hash_hmac('sha256', $user_id, $secret);
}
require 'openssl'
def compute_user_hash(secret, user_id)
OpenSSL::HMAC.hexdigest('SHA256', secret, user_id)
end

Sign a JWT with the HS256 algorithm using the chatbot secret. exp is required: Aihio rejects tokens without it. Clock skew tolerance is 30 seconds.

Choose a short lifetime suitable for your integration, for example one hour (exp = iat + 3600). This is an example choice, not a universal maximum enforced by the service. Include iat so that the server can also check the configured maximum token age.

Supported JWT claims
ClaimTypeRequiredNotes
user_id or substringyes (either one)Your system’s user ID
external_idstringnoSubject alias; equivalent to user_id/sub
expnumber (Unix timestamp)yesExpiry; 30 s leeway applied
iatnumber (Unix timestamp)RecommendedIssue time used to check maximum token age
nbfnumber (Unix timestamp)noNot-before; 30 s leeway applied
emailstringnoPre-fills the pre-chat form
namestringnoPre-fills the pre-chat form
phonenumberstringnoAccepted by the verifier; not a guarantee of form prefill
custom_attributesobjectnoArbitrary extra data
stripe_accountsarraynoUser’s Stripe accounts (see below)

Only HS256 is accepted. RS256, ES256, or alg: none tokens are rejected.

Sign the token with the chatbot secret using HS256. Set exp (1 hour recommended). custom_attributes is optional.

Install: npm install jsonwebtoken

const jwt = require('jsonwebtoken');
function signIdentityToken(secret, user) {
return jwt.sign(
{
user_id: user.id,
email: user.email,
name: user.name,
custom_attributes: { plan: user.plan },
},
secret,
{ algorithm: 'HS256', expiresIn: '1h' },
);
}

Install: bundle add jwt

require 'jwt'
def sign_identity_token(secret, user)
payload = {
user_id: user.id,
email: user.email,
name: user.name,
custom_attributes: { plan: user.plan },
exp: Time.now.to_i + 3600,
iat: Time.now.to_i,
}
JWT.encode(payload, secret, 'HS256')
end

Install: pip install PyJWT

import time
import jwt
def sign_identity_token(secret: str, user) -> str:
payload = {
'user_id': user.id,
'email': user.email,
'name': user.name,
'custom_attributes': {'plan': user.plan},
'exp': int(time.time()) + 3600,
'iat': int(time.time()),
}
return jwt.encode(payload, secret, algorithm='HS256')

Install: composer require firebase/php-jwt

use Firebase\JWT\JWT;
function sign_identity_token(string $secret, $user): string {
$payload = [
'user_id' => $user->id,
'email' => $user->email,
'name' => $user->name,
'custom_attributes' => ['plan' => $user->plan],
'exp' => time() + 3600,
'iat' => time(),
];
return JWT::encode($payload, $secret, 'HS256');
}

Install: go get github.com/golang-jwt/jwt/v5

import (
"time"
"github.com/golang-jwt/jwt/v5"
)
func SignIdentityToken(secret string, user User) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"email": user.Email,
"name": user.Name,
"custom_attributes": map[string]any{"plan": user.Plan},
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
})
return token.SignedString([]byte(secret))
}

Install (Maven): io.jsonwebtoken:jjwt-api, jjwt-impl, jjwt-jackson (runtime).

import io.jsonwebtoken.Jwts;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
String signIdentityToken(String secret, User user) {
SecretKeySpec key = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
return Jwts.builder()
.claim("user_id", user.getId())
.claim("email", user.getEmail())
.claim("name", user.getName())
.claim("custom_attributes", Map.of("plan", user.getPlan()))
.expiration(new Date(System.currentTimeMillis() + 3_600_000))
.issuedAt(new Date())
.signWith(key)
.compact();
}

The verifier accepts stripe_accounts, but this does not enable retrieving Stripe subscriptions or invoices. Do not send these records in anticipation of future functionality. Include only fields needed by your current integration.

Compute the proof server-side and pass it to the signed-in user through a protected response. Replace the placeholders below with those values, never the signing secret. Choose only one of the two methods:

// Method A: HMAC user-hash
window.AihioWidget('identify', {
externalId: currentUser.id, // 'user_id' is also accepted; externalId takes precedence.
email: currentUser.email,
name: currentUser.name,
user_hash: '{{ user_hash_from_server }}',
});
// Method B: JWT token
window.AihioWidget('identify', {
token: '{{ signed_jwt_from_server }}',
});

currentUser means your application’s authenticated user, not an arbitrary ID selected in the browser. Call identify after init. If the chat window has not loaded yet, the embed snippet queues the command for initialization.

Clear the identity on logout. This does not delete conversations stored by the service:

window.AihioWidget('resetUser');

Rotating a secret does not immediately invalidate the previous one. The previous secret remains valid for 24 hours after rotation. Update the signing configuration in every environment before that window ends.

Routine rotation when the secret has not been exposed

Section titled “Routine rotation when the secret has not been exposed”
  1. Rotate the secret

    Press Rotate under Settings → Security → Identity verification.

  2. Update the environment variable

    Update AIHIO_IDENTITY_SECRET in your server environment with the new value.

  3. Deploy

    Deploy the new environment variable. The old secret continues to verify sessions in parallel for 24 hours.

  4. Wait 24 hours

    After 24 hours the old secret stops being accepted. The vault entry is retained but no longer used for verification.

By default verification is fail-open. You can opt in to enforcement from the chatbot’s Settings → Security view.

  • Enforce identity verification. Any request that claims an identity (user_id/external_id, email, token, or user_hash) without a valid signature is rejected with HTTP 403. Anonymous visitors can still chat.
  • Require authentication (strict). Every unverified request is rejected, including anonymous ones.

When enforcement is on, a conversation is bound to the verified external_id. A reused reference_id whose stored identity does not match the verified token starts a fresh conversation instead of attaching, so one visitor cannot resume another visitor’s private conversation.

Enable enforcement only after a successful verification test. Test a valid JWT, an expired JWT and a visitor without identity separately. Check that the chosen anonymous-access policy matches your intention.

  • Secure transport only. When enabled, the chat window passes and stores identity only over HTTPS. On an HTTP page it omits identity; server enforcement settings determine whether anonymous conversation is allowed.
  • Session duration. Set a maximum accepted token age. The server can check age using a numeric iat claim. Without it, this age limit does not replace exp. Include both iat and exp in new tokens and test the chosen limit.

When enforcement is disabled, missing or failed verification allows the conversation to continue with identity_verified = false. Enforcement and strict authentication change that behavior as described above.

An unverified user_id or email is a caller-supplied display hint. The identity_verified value alone does not distinguish a verified JWT from a user-hash. An action involving private data needs JWT verification and a separate authorization check.

Only data signed inside the JWT is verified and safe to trust. Everything else is a display hint that a caller could forge.

Signed vs unsigned data
Signed (trusted)Unsigned (hint only)
user_id / external_idPrechat form name, email and consent
email, name, custom_attributes (when carried inside the JWT)Any x-identify header sent without a JWT

The HMAC user-hash (Method A) verifies only the external_id. To trust email, name or custom attributes, sign them inside a JWT (Method B). Caller-supplied fields are accepted as display hints but are never recorded as verified, and the conversation’s identity_verified flag stays false unless a valid signature is present.

  • Store the chatbot secret server-side only. Never include it in client JavaScript or commit it to version control.
  • Include iat and a short exp on every JWT.
  • If the secret is exposed, select Settings → Security → Identity verification → Remove, create a new key, deploy it, and verify the replacement before re-enabling enforcement. Do not use routine rotation to revoke an exposed secret.
  • Require JWT verification for actions involving private data. Also check in your own endpoint that the user may access the particular requested information.
  • Use lowercase hex output for HMAC (the default for all language functions above).

Last updated:

Send feedback by email