Developers
Identity verification (HMAC)
Sign customer identity on your server with HMAC-SHA256 so Sela can trust who is chatting, with the exact formula, limits and code samples.
Last updated: 2026-09-29
Identity verification lets your backend tell Sela who a logged-in customer is, so the chat opens as that person with no login form and their history follows them. Your server signs the customer's email with a shared secret using HMAC-SHA256. Sela recomputes the signature and, if it matches and is fresh, creates a verified session.
How it works#
- Your server generates a timestamp and a random nonce and signs
email|timestamp|orgId|noncewith the secret. - Your page opens the hosted widget URL (
https://widget.usesela.com/) with the customer's details and the signature as query parameters, inside your own iframe or a mobile WebView. - Sela verifies the signature, the timestamp window and the nonce, then creates a verified contact session.
- If verification fails, the widget falls back to the normal name-and-email form, unless you turned on strict mode (see below).
Get your secret#
- Open Integrations in the dashboard and find the Identity Verification card.
- Generate the secret. It is 32 random bytes shown as 64 hex characters.
- Copy it into your server's secret store. Never put it in browser code, a mobile app or a repository.
Regenerating the secret invalidates every integration that still signs with the old one. Update your servers first or expect verification failures until you do.
The signing formula#
user_hash = hex( HMAC_SHA256( secret, email + "|" + timestamp + "|" + orgId + "|" + nonce ) )| Part | Rule |
|---|---|
email | Exactly the string you send in the email parameter, before URL encoding. Sela does not lowercase it before checking the signature, so sign the same case you send. |
timestamp | Integer Unix time in milliseconds, the same digits as the timestamp parameter. Accepted skew is 5 minutes either side of Sela's clock. |
orgId | Your organization ID (the org_... value from the dashboard). |
nonce | Any string of at least 16 characters. A UUID v4 is recommended. Same value as the nonce parameter. |
| Output | Lowercase hex, 64 characters. |
Sela still accepts the older three-part message email|timestamp|orgId, but the nonce parameter is still required and you should use the four-part form.
A test vector to check your implementation: with secret s3cret, email user@example.com, timestamp 1712345678901, org ID org_123 and nonce 550e8400-e29b-41d4-a716-446655440000, the signature is:
0a2d9cfdc55170688df71fbbaefa64a785d788e4248afaa310a74281be2ffdf3That vector will be rejected by the live service because its timestamp is old. It only proves your HMAC code is correct.
Widget URL parameters#
Build the URL server-side and let your page put it in an iframe src (or open it in a WebView).
| Parameter | Required | Meaning |
|---|---|---|
orgId | Yes | Your organization ID. |
email | For identity | Customer email. |
user_hash | For identity | The hex signature. |
timestamp | For identity | Milliseconds, as signed. |
nonce | For identity | As signed. |
name | No | Display name. Defaults to the email. |
avatarUrl | No | HTTPS image URL for the contact avatar. Failures are ignored. |
theme | No | light or dark. |
primaryColor | No | Hex color for accents. |
support_locale | No | ar or en. |
initialScreen | No | chat opens the chat directly, or voice opens the voice screen. |
All four of email, user_hash, timestamp and nonce must be present to attempt verification. If any is missing the widget treats the visitor as anonymous.
Code samples#
Node.js#
import crypto from "node:crypto";
export function signedWidgetUrl({ orgId, secret, user }) {
const timestamp = Date.now(); // milliseconds
const nonce = crypto.randomUUID(); // at least 16 characters
const user_hash = crypto
.createHmac("sha256", secret)
.update(`${user.email}|${timestamp}|${orgId}|${nonce}`)
.digest("hex");
const url = new URL("https://widget.usesela.com/");
url.search = new URLSearchParams({
orgId,
email: user.email,
name: user.name ?? user.email,
timestamp: String(timestamp),
nonce,
user_hash,
}).toString();
return url.toString();
}Python#
import hashlib
import hmac
import time
import uuid
from urllib.parse import urlencode
def signed_widget_url(org_id: str, secret: str, email: str, name: str | None = None) -> str:
timestamp = int(time.time() * 1000) # milliseconds
nonce = str(uuid.uuid4())
message = f"{email}|{timestamp}|{org_id}|{nonce}".encode()
user_hash = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
query = urlencode({
"orgId": org_id,
"email": email,
"name": name or email,
"timestamp": timestamp,
"nonce": nonce,
"user_hash": user_hash,
})
return f"https://widget.usesela.com/?{query}"PHP#
<?php
function signedWidgetUrl(string $orgId, string $secret, string $email, ?string $name = null): string {
$timestamp = (int) round(microtime(true) * 1000); // milliseconds
$nonce = bin2hex(random_bytes(16)); // 32 characters
$hash = hash_hmac('sha256', "{$email}|{$timestamp}|{$orgId}|{$nonce}", $secret);
return 'https://widget.usesela.com/?' . http_build_query([
'orgId' => $orgId,
'email' => $email,
'name' => $name ?? $email,
'timestamp' => $timestamp,
'nonce' => $nonce,
'user_hash' => $hash,
]);
}Using the URL in your page#
<!-- Render server-side: the src comes from signedWidgetUrl() -->
<iframe
src="SIGNED_URL_FROM_YOUR_SERVER"
title="Support chat"
allow="microphone; clipboard-read; clipboard-write"
style="width: 400px; height: 600px; border: 0"
></iframe>Generate the URL on every page or session load. A URL is only valid for five minutes from its timestamp, so do not cache it or store it in a template that outlives that window. In a React Native WebView, pass the same URL as the source.uri.
What Sela checks#
Sela runs these checks in order and stops at the first failure:
- The workspace has the AI Assist add-on.
- The (organization, email) pair is not rate limited.
- An identity secret exists for the organization.
- The timestamp is within 5 minutes of Sela's clock.
- The nonce is at least 16 characters.
- The signature matches (constant-time comparison).
- The nonce state: first use continues, an already completed nonce returns the same session, and a nonce still being processed waits up to 5 seconds.
- The organization exists and is not blocked.
A verified customer gets a contact session marked as identity verified, valid for 24 hours by default. If the same email already has an active verified session, Sela reuses it instead of creating another, so chat history continues.
Nonces and reloads#
A nonce is not an error the second time. Reloading the page with the same signed URL within the timestamp window resumes the same session. Only a different signed request needs a fresh nonce, and generating a fresh timestamp and nonce for every page load takes care of that.
Rate limits and blocking#
Failed verifications are counted per organization and email. Five failures within a 15-minute window block that pair for 30 minutes. A successful verification clears the counter. While blocked, the widget reports "Too many attempts" with the number of seconds to wait.
Strict mode: disable manual login#
By default a failed or missing signature falls back to the normal name-and-email form. To make signed access the only way in, open Integrations, go to the Identity Verification card and turn on Disable Manual Login (Strict Mode). The setting is stored as disableManualLogin.
In strict mode:
- A failed verification shows an "Authentication required" error instead of the form.
- Visitors who arrive with no identity parameters and no existing session see "Authentication required. Please access support from within your app."
Only turn it on once your signed flow works in production, and keep a plan for staff testing: strict mode also blocks anonymous testing of the widget.
Troubleshooting#
| Message or symptom | Cause and fix |
|---|---|
| Identity verification requires AI Assist plan or higher | The workspace does not have the AI Assist add-on. |
| Widget settings not found for this organization | The orgId is wrong. Copy it again from the dashboard. |
| Identity verification not configured. Please set up identity secret in dashboard. | No secret has been generated yet. |
| Request expired. Please try again. | The timestamp is more than 5 minutes off. Check that you send milliseconds (not seconds) and that your server clock is synced. |
| Invalid nonce format. | The nonce is shorter than 16 characters. |
| Invalid signature. Authentication failed. | The signed string differs from the URL values: wrong order, different email case, a URL-encoded email in the signature, timestamp in seconds, wrong secret, or a secret that was regenerated. Compare against the test vector. |
| Too many attempts. Please try again in N seconds. | Five failures in 15 minutes. Fix the cause and wait for the block to end. |
| Authentication already in progress. Please try again. | A parallel request with the same nonce was still running. Retry once. |
| Organization not found or blocked. | The workspace is not active. Check the account status. |
| Widget shows the login form | Verification failed or one of the four parameters is missing. Check the parameter names (user_hash, not userHash). |
Name or email with a literal % looks wrong | The widget decodes email and name once more after the browser decodes the query string, so avoid literal % characters in those values. |
Security checklist#
- Sign on the server only. Anyone who has the secret can impersonate any customer.
- Use a fresh timestamp and nonce for every signed URL.
- Sign only emails you have authenticated in your own system.
- Rotate the secret from Integrations if it may have leaked, then update your servers immediately.
- Keep the widget iframe on pages that only logged-in users can reach.
Can't find what you need? Contact support.